{
  "openapi": "3.1.0",
  "info": {
    "title": "Open Climate AI API",
    "description": "Open Climate AI turns a procurement or spend ledger into a greenhouse-gas\ninventory. You send a CSV or an Excel workbook; the API reads its shape, matches\nevery line to an emission factor from a published library, and hands back the\nmatched rows, the emissions totals and a workbook your finance team can open.\n\nThe API is asynchronous by nature. A ledger of a few thousand lines is minutes\nof retrieval and review, so a mapping job is queued rather than answered inline,\nand progress is reported on a server-sent-event stream or on a polling endpoint\nthat returns the same numbers.\n\nThis reference documents `/v1`, which is the whole public surface. The\nadministration portal's API is not part of it and is not published here.\n\n## A run, end to end\n\n1. **Upload the ledger.** `POST /v1/uploads` from your backend, or\n   `POST /v1/upload-slots` to mint a one-shot URL the browser posts to without\n   ever holding your key. Either way you end up with an `upload_id`.\n2. **Inspect it.** `POST /v1/uploads/{upload_id}/inspect` reports the sheet, the\n   header row, the columns worth reading and the currency and country it\n   inferred, plus any question it could not answer on its own. Show the answers\n   to your user, or accept them and move on.\n3. **Queue the job.** `POST /v1/mapping-jobs` with the `upload_id` and whatever\n   you corrected. Everything but `upload_id` is optional; omit a field and the\n   server uses its own best guess. It answers `202` with a `job_id` and an\n   `events_url`.\n4. **Watch it.** Connect to `GET /v1/mapping-jobs/{job_id}/events` for the\n   stream, or poll `GET /v1/mapping-jobs/{job_id}` and honour\n   `retry_after_seconds`. The two report identical state on purpose.\n5. **Take the result away.** `GET /v1/mapping-jobs/{job_id}/result` for the\n   statistics and the download links, `/rows` for JSON, `/rows.csv` for CSV,\n   `/file` for the workbook.\n\n`GET /v1/libraries` lists the factor libraries you may restrict a job to. Call\nit before step 3 if you want a `libraries` allow-list; skip it and the job\nsearches everything.\n\n## Authentication\n\nEvery `/v1` route requires a bearer token. There are three ways to hold one, and\nwhich you use depends on what you are building.\n\n### Partner key plus subject (the integration path)\n\nThis is the path a software vendor integrates. Your key identifies **you**; a\nper-request header identifies **which of your customers** the call is for.\n\n```http\nAuthorization: Bearer ocai_live_xxxxxxxxxxxxxxxxxxxxxxxx\nX-OCAI-Subject: customer-4711\n```\n\nBoth are required together. A partner key without `X-OCAI-Subject` is rejected\nwith `401`, because there would be no way to tell whose data the request meant.\n\nThe subject is opaque to us: send your own stable customer identifier, or the\nend user's email address if that is what you have. It is not a credential and\nit is never validated against a directory. What it does is scope everything.\nUploads, jobs and results created under one subject are invisible to every\nother subject, including other subjects of your own partner account. Send the\nsame value for the same customer every time, or that customer loses their\nhistory.\n\nKeys are issued by us, per partner, and look like `ocai_live_...`. Ask at\n[hello@open-climate.ai](mailto:hello@open-climate.ai). Treat a key like a\npassword: it belongs on your server, never in a browser bundle or a mobile app.\n\n**Derive `X-OCAI-Subject` on your server, from your own session.** Because the\nsubject is what scopes the data and is not itself authenticated, a request that\nforwards a subject supplied by the browser lets any of your users read any other\nuser's ledgers by editing one header. The key authenticates you; the subject is\nyour assertion about who you are acting for, and it has to be an assertion you\nmade, not one you relayed.\n\n### User key\n\nA single-tenant key that carries its own identity. No `X-OCAI-Subject`; the key\n*is* the subject. This is what you want for a script, a notebook or an internal\njob that only ever acts for one account.\n\n### OAuth 2.1\n\nInteractive applications authenticate a real person against our Keycloak realm\nand send the resulting access token as the bearer. Use this when a human is at\nthe keyboard and you would otherwise be storing a shared secret on their behalf.\n\n## Trying a request from this page\n\nEvery operation below has a **Test Request** button that sends a real request\nfrom your browser to the server you pick in the dropdown. There are no demo\ncredentials: it uses your own key.\n\nOpen the authentication panel, choose **Partner key + subject**, paste your\n`ocai_live_...` key and put any stable string in `X-OCAI-Subject`. Both persist\nin your browser's local storage, so you set them once. Pick the sandbox server\nfirst if you would rather not create jobs against production.\n\n`GET /v1/libraries` is the cheapest thing to try: it takes no arguments, and a\n`200` proves your key, your subject header and your network path all work.\n\nThe event stream is the one operation the console renders as it arrives rather\nthan after it ends. If you would rather watch it in a terminal, the `curl -N`\nline on that operation does the same thing with your own credentials.\n\n## Errors\n\nEvery failure under `/v1` has the same body, whatever went wrong:\n\n```json\n{\n  \"error\": {\n    \"code\": \"job_not_found\",\n    \"message\": \"No mapping job 9f1c8e0a-... for this user.\",\n    \"status\": 404,\n    \"request_id\": \"01JZ8QH4M2T7N0S4G6VQK8XBRD\"\n  }\n}\n```\n\nWrite one error handler. Branch on `error.code`, show `error.message`, and log\n`request_id`: it is echoed on the `X-Request-Id` response header of *every*\nresponse, successful ones included, and it is the handle that lets us find your\nrequest in our traces. Quote it in any report you send us.\n\nTwo rules a client has to implement:\n\n- **Ignore codes you do not recognise.** The list is stable, not closed. New\n  codes are added for conditions that did not exist before, and treating an\n  unknown code as fatal turns an additive change into an outage. Fall back on\n  the HTTP status and `error.message`.\n- **A resource belonging to another subject answers `404`, not `403`.** This is\n  deliberate. A `403` would confirm that the id exists somewhere, which is a\n  probing oracle across tenants. So `job_not_found` means \"no such job for\n  *this* subject\" and covers both cases. If you are sure the id is right, check\n  the `X-OCAI-Subject` you sent before you go looking for a bug.\n\n### Status codes\n\n| Status | Codes | What to do |\n| --- | --- | --- |\n| `400` | `invalid_request` | Fix the request. Schema violations land here too, not on a `422`. |\n| `401` | `unauthenticated` | Missing, unknown or revoked key, or a partner key with no `X-OCAI-Subject`. |\n| `403` | `forbidden` | Authenticated, but not for this. `/v1` does not use it for resource scoping; see the `404` rule above. |\n| `404` | `upload_not_found`, `job_not_found`, `user_not_found` | No such id **for this subject**. |\n| `409` | `job_not_ready`, `job_failed` | The job exists but has no result yet, or has none at all. Watch the stream instead of retrying blind. |\n| `413` | `file_too_large` | Split the ledger and upload the parts. |\n| `422` | `unreadable_file` | The bytes landed but could not be parsed as a CSV or a workbook. |\n| `429` | `rate_limited` | Back off. The response carries `Retry-After` in seconds; wait that long before the next call. |\n| `500` | `internal_error` | Ours. Report it with the `request_id`. |\n| `503` | `service_unavailable` | The process is still starting. Retry with backoff; it clears on its own. |\n\n## Messages and clarifications\n\nResponses that have something to say to a human carry a `messages` array. Each\nentry has a `level` (`info`, `warning` or `action_required`), a stable `code`\nfor translation and suppression, English `text` that is safe to render as-is,\nand the request fields the message concerns so your UI can highlight the right\ncontrol. Only `action_required` should ever block your user.\n\nThe inspect response may also carry `clarification`: concrete questions with\nconcrete options, for the handful of things that cannot be inferred safely (the\nsheet to read, the header row, the currency of a spend column). Answering them\nis optional; the job runs on the server's own guess if you do not.\n\nMessage codes are subject to the same rule as error codes: ignore the ones you\ndo not know.\n\n## Conventions\n\n- **Identifiers** are UUIDs, always sent as strings.\n- **Timestamps** are RFC 3339 in UTC, for example `2026-08-31T09:12:44.517Z`.\n- **`client_reference`** is yours. Set it on job creation and it comes back on\n  every payload and every progress event for that job, which is how you join\n  our job to your record without keeping a mapping table.\n- **`X-Request-Id`** is on every response. Log it.\n- **Absolute URLs** in responses (`events_url`, the `downloads` block) are ready\n  to use as returned. Do not rebuild them from parts.\n- **Download links** minted with `?as=link` carry their own signed token and\n  need no bearer header, so they can go straight into a browser. They expire.\n  Hand one to a user; never store one.\n\n## Versioning\n\nThe path prefix carries the major version. Inside `/v1` we add, we do not\nremove: new endpoints, new optional request fields, new response fields, new\nenum members and new message codes can all appear without warning, and a client\nthat ignores what it does not recognise will not notice. Removing a field,\nchanging a type or tightening validation would be a new prefix.",
    "contact": {
      "name": "Open Climate AI",
      "url": "https://open-climate.ai/",
      "email": "hello@open-climate.ai"
    },
    "license": {
      "name": "Proprietary"
    },
    "version": "1.0.0",
    "x-logo": {
      "url": "https://open-climate.ai/icon.svg",
      "altText": "Open Climate AI"
    }
  },
  "servers": [
    {
      "url": "https://api.open-climate.ai",
      "description": "Production"
    },
    {
      "url": "https://dev.open-climate.ai",
      "description": "Sandbox. Same contract, separate data, smaller machine."
    }
  ],
  "paths": {
    "/v1/uploads": {
      "post": {
        "tags": [
          "uploads"
        ],
        "summary": "Upload a ledger",
        "description": "Send the ledger with the request, as `multipart/form-data` under the field name\n`file`. One round trip, the bytes never touch a browser, and the `upload_id` you\nget back is what every later call names. This is the way in for a backend\nintegration; reach for `POST /v1/upload-slots` only when the file has to leave\nyour user's machine without passing through your servers.\n\nCSV and Excel workbooks are what the pipeline reads. The upload itself stores\nwhatever you send and does not judge it: a file that cannot be parsed is\nreported by `inspect`, as `unreadable_file`, rather than being rejected here.\nThat way an upload never fails for a reason you would have to guess at.\n\nThe response repeats `max_bytes`, the deployed ceiling, so you can size a\nclient-side check against the server you are actually talking to instead of\nhard-coding ours. Over it, the request fails with `file_too_large` (413); split\nthe ledger and upload the parts as separate jobs.\n\nAn upload belongs to the `X-OCAI-Subject` that created it and to no one else. It\nis single use in the sense that matters: nothing stops you queueing two jobs\nfrom one upload, but each job reads the file again from storage as it was.",
        "operationId": "create_upload_v1_uploads_post",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_create_upload_v1_uploads_post"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadResponse"
                },
                "example": {
                  "upload_id": "5f0f0bd4-6b4c-4a1e-9a13-6d0f4c2f8b71",
                  "status": "uploaded",
                  "filename": "ledger.csv",
                  "file_path": "s3://ocai-uploads/uploads/9a3c1e77-2f4b-4d81-b1c0-7e2a5d9f0c34/ledger.csv",
                  "content_type": "text/csv",
                  "size_bytes": 117,
                  "created_at": "2026-03-04T09:12:41Z",
                  "uploaded_at": "2026-03-04T09:12:42Z",
                  "max_bytes": 104857600,
                  "messages": []
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      }
    },
    "/v1/upload-slots": {
      "post": {
        "tags": [
          "uploads"
        ],
        "summary": "Mint a browser upload URL",
        "description": "Mint a one-shot URL that a browser can `POST` a file to directly, with no\npartner key anywhere near it. The URL carries its own scoped, expiring\ncredential; it is bound to this one upload and this one subject, and it lapses\nan hour after it is minted.\n\nUse it only for the case it exists for: the bytes must go straight from the\nuser's machine to us, and routing them through your backend is not an option.\nAnything else should use `POST /v1/uploads`, which is one call instead of three.\n\n**Never send your bearer token to this URL.** It needs none, and a partner key\nposted from a browser is a partner key you have published. The response also\ncarries `file_prefix` and `expires_at`; the prefix is ours, and you need not do\nanything with it.\n\nThis is not a pre-signed storage URL. It points at our API, which authenticates\nthe capability, enforces the size ceiling and records the upload, so the\n`upload_id` you were handed becomes usable the moment the request completes.\n\nAfter handing the URL to the browser, poll `GET /v1/uploads/{upload_id}` until\nit reports `uploaded`. Uploading the same filename twice is a safe retry;\nuploading a different filename to the same slot is rejected, because the slot\nnames one file and quietly replacing it would change what a job you already\nqueued is about to read.\n\nThe capability URL is not a `/v1` route, so what comes back from it is a plain\nHTTP status rather than the error envelope documented here.",
        "operationId": "create_upload_slot_v1_upload_slots_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UploadSlotRequest"
                  },
                  {
                    "type": "null"
                  }
                ],
                "title": "Payload"
              },
              "example": {
                "filename": "ledger.csv"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadSlotResponse"
                },
                "example": {
                  "upload_id": "c1b9a4d2-8e35-4f60-9c27-0a4d1b6e3f58",
                  "upload_url": "https://api.open-climate.ai/files/upload?upload_id=c1b9a4d2-8e35-4f60-9c27-0a4d1b6e3f58&user=7e64d0a9-13cf-4b52-8d70-2f9a6c1e4b03&t=1788177600.EXAMPLE-CAPABILITY-SIGNATURE",
                  "file_prefix": "s3://ocai-uploads/uploads/3b8f2c60-5a97-4e13-8f4b-1c6d0e9a72b5/",
                  "expires_at": "2026-03-04T09:42:42Z",
                  "messages": []
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      }
    },
    "/v1/uploads/{upload_id}": {
      "get": {
        "tags": [
          "uploads"
        ],
        "summary": "Check whether an upload has landed",
        "description": "Report whether the bytes have landed. This is the poll that follows a\ncapability upload; a file sent to `POST /v1/uploads` is already `uploaded` by\nthe time you hold its id, so there is nothing here to wait for.\n\n`status` is one of:\n\n| Value | Meaning |\n| --- | --- |\n| `pending` | The slot exists and is still open. Nothing has been posted yet. |\n| `uploaded` | The bytes are stored. `filename` and `size_bytes` are filled in and the upload can be inspected and mapped. |\n| `expired` | The hour ran out with nothing posted. Mint a new slot. |\n\nBytes that arrived do not lapse: an upload that reached `uploaded` stays\n`uploaded` past its expiry, because the credential expiring is about who may\nstill write to the slot, not about how long the file lives.\n\nAn unknown id, or one belonging to another subject, answers `404`\n`upload_not_found`.",
        "operationId": "read_upload_v1_uploads__upload_id__get",
        "parameters": [
          {
            "name": "upload_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Upload Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadStatusResponse"
                },
                "example": {
                  "upload_id": "5f0f0bd4-6b4c-4a1e-9a13-6d0f4c2f8b71",
                  "status": "uploaded",
                  "filename": "ledger.csv",
                  "size_bytes": 117,
                  "created_at": "2026-03-04T09:12:41Z",
                  "uploaded_at": "2026-03-04T09:12:42Z",
                  "messages": []
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      }
    },
    "/v1/uploads/{upload_id}/inspect": {
      "post": {
        "tags": [
          "uploads"
        ],
        "summary": "Inspect a file's shape",
        "description": "Read the file and report what shape it is, before committing to a run. This is\nthe step that makes the mapping deterministic: everything it returns is\nsomething you can confirm with your user and send straight back on\n`POST /v1/mapping-jobs`.\n\nYou get, per sheet, the row count, the detected header row and the columns with\ntheir inferred roles; then a single `suggested` block, which is the server's own\nanswer to \"what would I run if you told me nothing\". `useful_columns` in that\nblock is a whitelist of the columns the matcher will read, so a ledger with\nforty columns of internal accounting does not drag them all through retrieval.\n\n`file_context` inside `suggested` is the currency and the buyer country the\nserver inferred. Both change the answer: a spend line in the wrong currency is\nwrong by the exchange rate, and the buyer country picks the grid and the\nregional factors. Where the inference was not confident, the response also\ncarries `clarification`, which is a list of concrete questions with concrete\noptions, each naming the request field its answer belongs in. Show them, or\nignore them and let the server use its guess.\n\nSet `skip_file_context` to `true` to skip the currency and country inference. It\nsaves roughly half a second and is the right choice when you already know both\nand are going to send them anyway.\n\nRow indices here and everywhere else on the wire are **0-based**, and `0` means\nthe first row of the file. End-user message text counts from 1, because that is\nwhat a spreadsheet shows.\n\n`unreadable_file` (422) is where a file that is not really a CSV or a workbook\nsurfaces, whatever it was called when it was uploaded.",
        "operationId": "inspect_upload_v1_uploads__upload_id__inspect_post",
        "parameters": [
          {
            "name": "upload_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Upload Id"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/InspectRequest"
                  },
                  {
                    "type": "null"
                  }
                ],
                "title": "Payload"
              },
              "example": {
                "skip_file_context": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InspectResponse"
                },
                "example": {
                  "upload_id": "5f0f0bd4-6b4c-4a1e-9a13-6d0f4c2f8b71",
                  "filename": "ledger.csv",
                  "format": "csv",
                  "size_bytes": 117,
                  "confidence": 0.9,
                  "sheets": [
                    {
                      "name": "(csv)",
                      "n_rows": 3,
                      "total_rows": 3,
                      "sampled": false,
                      "n_cols": 5,
                      "looks_like_data": true,
                      "looks_like_data_reason": "5 text + 0 numeric columns across 2 body rows",
                      "header_candidates": [
                        {
                          "row_index": 0,
                          "confidence": 0.9,
                          "reason": "all-string cells; distinct values; covers 100% of columns; body density 100%"
                        },
                        {
                          "row_index": 1,
                          "confidence": 0.9,
                          "reason": "all-string cells; distinct values; covers 100% of columns; body density 100%"
                        }
                      ],
                      "preview_rows": [
                        [
                          "Description",
                          "Quantity",
                          "Unit",
                          "Amount",
                          "Currency"
                        ],
                        [
                          "A4 copier paper 80gsm",
                          "10",
                          "kg",
                          "120.50",
                          "EUR"
                        ],
                        [
                          "Road diesel B7",
                          "200",
                          "litre",
                          "340.00",
                          "EUR"
                        ]
                      ],
                      "column_profile": [
                        {
                          "name": "Description",
                          "dtype": "string",
                          "pct_non_null": 1.0,
                          "samples": [
                            "A4 copier paper 80gsm",
                            "Road diesel B7"
                          ]
                        },
                        {
                          "name": "Quantity",
                          "dtype": "number",
                          "pct_non_null": 1.0,
                          "samples": [
                            "10",
                            "200"
                          ]
                        },
                        {
                          "name": "Unit",
                          "dtype": "string",
                          "pct_non_null": 1.0,
                          "samples": [
                            "kg",
                            "litre"
                          ]
                        },
                        {
                          "name": "Amount",
                          "dtype": "number",
                          "pct_non_null": 1.0,
                          "samples": [
                            "120.50",
                            "340.00"
                          ]
                        },
                        {
                          "name": "Currency",
                          "dtype": "string",
                          "pct_non_null": 1.0,
                          "samples": [
                            "EUR",
                            "EUR"
                          ]
                        }
                      ]
                    }
                  ],
                  "suggested": {
                    "sheet_name": "(csv)",
                    "header_row": 0,
                    "skip_trailing_rows": 0,
                    "useful_columns": [
                      "Description",
                      "Quantity",
                      "Unit",
                      "Amount",
                      "Currency"
                    ],
                    "file_context": null
                  },
                  "messages": [
                    {
                      "level": "info",
                      "code": "sheet_selected",
                      "text": "Using the sheet (csv).",
                      "fields": [
                        "sheet_name"
                      ]
                    },
                    {
                      "level": "info",
                      "code": "header_row_selected",
                      "text": "Row 1 of (csv) was read as the column header.",
                      "fields": [
                        "header_row"
                      ]
                    }
                  ],
                  "clarification": {
                    "required": false,
                    "questions": []
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      }
    },
    "/v1/libraries": {
      "get": {
        "tags": [
          "libraries"
        ],
        "summary": "List the emission-factor libraries",
        "description": "The emission-factor libraries a job may be restricted to, with the `code` you\npass back in `libraries`. Render your picker from this endpoint rather than\nhard-coding the list: libraries are added, and one that exists in the sandbox\nbefore production is normal.\n\nThe codes are stable and are the contract. Each one can cover several internal\ndataset names, which is why a result row's `final_top_1_ef_data_source` is the\npublisher's own label rather than the code you filtered on.\n\n`factor_count` is the current edition of each dataset, so it reflects what a\nsearch can actually reach rather than the publisher's lifetime output. Treat it\nas indicative: it moves at ingest, and it is cached for the life of the process.\n\nTenant-private libraries are out of scope for `v1`. Everything listed here is a\npublished dataset, available to every partner on the same terms.",
        "operationId": "list_libraries_v1_libraries_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LibraryCatalogueResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      }
    },
    "/v1/mapping-jobs": {
      "post": {
        "tags": [
          "mapping-jobs"
        ],
        "summary": "Queue a mapping job",
        "description": "Queue the run. Answers `202` immediately with a `job_id` and an absolute\n`events_url`; the matching itself is minutes of work and happens on our side.\n\n`upload_id` is the only required field, and a request carrying nothing else is a\nperfectly good one: the server inspects the file and uses its own best guess for\neverything you left out. Send back whatever your user corrected on the inspect\nresponse, and nothing more.\n\n**Send `upload_id`, never a path or a URL.** The `file_path` and `upload_url`\nvalues you saw earlier are ours, and a job will not take them.\n\n### Restricting the libraries searched\n\n`libraries` is an allow-list of the codes from `GET /v1/libraries`. Omit it, or\nsend `null`, to search everything, which is the default and usually right. An\nempty list is rejected: it is not a way to say \"none\", and a search over nothing\nwould return nothing while looking like it worked. The response echoes\n`libraries_applied` with the default expanded, so you can always see what the\njob will actually search.\n\nNarrowing is not free, and this is the field most likely to make a run worse\nthan it needed to be. Each kind of line is routed to the libraries that can\nanswer it, and your allow-list is intersected with that routing rather than\nreplacing it. Drop `exiobase` and spend lines lose the fallback they rely on\nwhen nothing physical matches. Where the intersection comes out empty for a\nline, that line comes back unmatched with a `no_match_reason` of\n`library_filtered_out` rather than failing the job, so a filter that was too\ntight is visible in the result instead of being silent. Where the narrowing is\nmaterial, the `202` itself carries a `library_filter_narrows_route` message.\n\n### The rest of the plan\n\n`header_row`, `sheet_name`, `skip_trailing_rows` and `useful_columns` are the\ninspect suggestions, confirmed. `useful_columns` is a whitelist: leaving out a\ncolumn the matcher needs degrades every row, so trim it only to drop noise.\n\n`file_context.measure_pairs` is three-valued and the distinction matters. Omit\nit, or send `null`, and the server nominates the pairs itself. Send an explicit\n`[]` and it takes that as a deliberate abstention: no pair binding, and no\nfallback nomination either. Send pairs and they are used verbatim. Never invent\none, because a wrong pair is worse than no pair.\n\n`client_reference` is yours, up to 128 characters. It comes back on every\npayload and every progress event for this job, which is how you join our job to\nyour record without keeping a table of ids.",
        "operationId": "create_job_v1_mapping_jobs_post",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateMappingJobRequest"
              },
              "example": {
                "upload_id": "5f0f0bd4-6b4c-4a1e-9a13-6d0f4c2f8b71",
                "header_row": 0,
                "file_context": {
                  "currency": "EUR",
                  "country": "GB",
                  "measure_pairs": [
                    {
                      "kind": "quantity_unit_column",
                      "quantity_column": "Quantity",
                      "unit_column": "Unit",
                      "granularity": "total"
                    },
                    {
                      "kind": "amount_currency_column",
                      "quantity_column": "Amount",
                      "unit_column": "Currency",
                      "granularity": "total"
                    }
                  ],
                  "source": "user"
                },
                "client_reference": "cad-2026-03-ledger-04"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateMappingJobResponse"
                },
                "example": {
                  "job_id": "e0472d18-9c3b-4a06-b52f-8d71c4e9350a",
                  "client_reference": "cad-2026-03-ledger-04",
                  "status": "queued",
                  "submitted_at": "2026-03-04T09:13:02Z",
                  "queue_position": 0,
                  "total_rows_estimate": 2,
                  "events_url": "https://api.open-climate.ai/v1/mapping-jobs/e0472d18-9c3b-4a06-b52f-8d71c4e9350a/events",
                  "libraries_applied": [
                    "ademe",
                    "agribalyse",
                    "aib",
                    "co2emissiefactoren_nl",
                    "desnz",
                    "ecoinvent",
                    "epa",
                    "exiobase",
                    "glec",
                    "miterd",
                    "uba_at",
                    "uba_de"
                  ],
                  "messages": [
                    {
                      "level": "info",
                      "code": "job_queued",
                      "text": "2 lines queued for matching. This usually takes a few minutes.",
                      "fields": []
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": {
                    "code": "upload_not_found",
                    "message": "No upload b57e1c93-40da-4f28-9e61-05a3d7b28c46 for this user.",
                    "status": 404,
                    "request_id": "01JAV7X4M2QK8B5NT3RCEZ9WPD"
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      },
      "get": {
        "tags": [
          "mapping-jobs"
        ],
        "summary": "List recent jobs",
        "description": "The newest jobs for the subject the request acts as, newest first. Scoped to\n`X-OCAI-Subject`, so this is the history you can safely render on one customer's\nscreen and never another's.\n\nFilter with `status` and cap with `limit` (1 to 200, default 20). There is no\ncursor: this is a recent-activity list, not an export. If you need every job you\nhave ever run, keep the ids as you create them.",
        "operationId": "list_jobs_v1_mapping_jobs_get",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 200,
              "minimum": 1,
              "default": 20,
              "title": "Limit"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "enum": [
                    "queued",
                    "running",
                    "succeeded",
                    "failed"
                  ],
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Return only jobs in this state.",
              "title": "Status"
            },
            "description": "Return only jobs in this state."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobListResponse"
                },
                "example": {
                  "jobs": [
                    {
                      "job_id": "e0472d18-9c3b-4a06-b52f-8d71c4e9350a",
                      "client_reference": "cad-2026-03-ledger-04",
                      "filename": "ledger.csv",
                      "status": "succeeded",
                      "row_count": 2,
                      "submitted_at": "2026-03-04T09:13:02Z",
                      "finished_at": "2026-03-04T09:13:11Z",
                      "total_emissions_kgco2e": 548.98
                    }
                  ],
                  "messages": []
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      }
    },
    "/v1/mapping-jobs/{job_id}": {
      "get": {
        "tags": [
          "mapping-jobs"
        ],
        "summary": "Poll a job",
        "description": "One snapshot of a job: its status, its stage, the counters and any messages.\n\nPrefer the event stream. This endpoint exists for clients that cannot hold a\nconnection open, and it is rendered by the same function as the stream's\n`snapshot` event, so the two cannot disagree.\n\nHonour `retry_after_seconds` between calls: 15 while the job is running, 0 once\nit is terminal. `percent` is progress within the current stage rather than\nacross the job, so it can move backwards at a stage boundary. `auditing` in\nparticular sits at 100 percent for as long as it takes and can go a couple of\nminutes without a counter moving; `seconds_since_last_progress` growing is not\nby itself a stuck job.\n\n`status` reaching `succeeded` is the signal to call\n`GET /v1/mapping-jobs/{job_id}/result`. On `failed`, `error_message` says what\nhappened and the result endpoints answer `409 job_failed`.",
        "operationId": "read_job_v1_mapping_jobs__job_id__get",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Job Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStatusResponse"
                },
                "example": {
                  "job_id": "e0472d18-9c3b-4a06-b52f-8d71c4e9350a",
                  "client_reference": "cad-2026-03-ledger-04",
                  "status": "succeeded",
                  "stage": null,
                  "processed_rows": 2,
                  "total_rows": 2,
                  "percent": 100.0,
                  "queue_position": null,
                  "submitted_at": "2026-03-04T09:13:02Z",
                  "started_at": "2026-03-04T09:13:03Z",
                  "finished_at": "2026-03-04T09:13:11Z",
                  "last_progress_at": "2026-03-04T09:13:10Z",
                  "elapsed_running_seconds": 8.4,
                  "seconds_since_last_progress": 0.6,
                  "retry_after_seconds": 0.0,
                  "error_message": null,
                  "messages": []
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      }
    },
    "/v1/mapping-jobs/{job_id}/events": {
      "get": {
        "tags": [
          "mapping-jobs"
        ],
        "summary": "Stream a job's progress",
        "description": "Follow a job to its end on one connection. Standard `text/event-stream` framing,\nwith nothing bespoke about it, so any SSE client will read it.\n\nA browser's built-in `EventSource` is the one client that will not: it cannot\nset request headers, and every `/v1` route needs `Authorization` and\n`X-OCAI-Subject`. Proxy this endpoint through your own backend, which is what\nmost integrations do anyway because it keeps the key server-side, or use a\n`fetch`-based SSE client that can set both.\n\nThe stream opens with a `snapshot` and closes after a terminal event. Connecting\nto a job that already finished is valid and cheap: you get the snapshot, the\nterminal event, and the close.\n\n### Events\n\n| `event` | Payload | Meaning |\n| --- | --- | --- |\n| `snapshot` | `JobProgressEvent` | Current state, sent immediately on connect. |\n| `stage` | `JobProgressEvent` | The pipeline moved to a new stage. |\n| `progress` | `JobProgressEvent` | Counters moved within the current stage. |\n| `succeeded` | `JobSucceededEvent` | Terminal. Carries the whole result body, so no follow-up call is needed. |\n| `failed` | `JobFailedEvent` | Terminal. Carries the same `error` object a failing request would have returned. |\n\nThe three progress payloads are the polling body plus one flattened `message`\nstring, so a progress bar has a single line to render without choosing from an\narray.\n\n**`percent` can move backwards.** It is progress within the current stage, not\nacross the job: `parsing` counts unique descriptions and `mapping` counts rows,\nso the number resets at the boundary between them. That is the pipeline's own\naccounting reported faithfully, rather than smoothed into a monotonic number\nthat would be a lie about what is happening.\n\n`auditing` is the stage that looks stuck and is not. It sits at 100 percent for\nits whole duration and can go a couple of minutes between events on a large\nledger, so treat a growing `seconds_since_last_progress` there as normal.\n\n### Reconnecting\n\nReconnect naively. The `snapshot` you get on connect and the body of\n`GET /v1/mapping-jobs/{job_id}` are rendered by the same function, so a client\nthat drops mid-run and comes back cannot observe numbers that disagree with the\nones it would have seen had it stayed connected. Take the latest event as truth\nand discard whatever you had accumulated.\n\n`Last-Event-ID` continues the id sequence rather than replaying the events in\nbetween, because every one of them is a state report and the snapshot already\ncarries the latest state. The server sends `retry: 3000` once, before the\nfirst event, which is what an `EventSource` uses to pace its own reconnect.\n\nComment frames arrive every 15s while nothing changes, so\nproxies do not reap an idle socket. The store is polled every 1s\nand only changes are emitted, which is also what coalesces a burst of pipeline\ncallbacks into one `progress` event.\n\n### Errors\n\nA job that does not exist, or belongs to another subject, fails **before** the\nstream opens: you get an ordinary `404` with the JSON error envelope, not a\nstream that ends immediately. Anything that goes wrong after the stream is open\narrives as a `failed` event.",
        "operationId": "stream_events_v1_mapping_jobs__job_id__events_get",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Job Id"
            }
          },
          {
            "name": "Last-Event-ID",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Last-Event-Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The open stream. Each frame is one `event:` name and one `data:` line of JSON; see the table above for which payload goes with which name.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "A server-sent-event stream. OpenAPI 3.1 has no way to type the individual events, so the payload schemas are published as components instead: JobProgressEvent, JobSucceededEvent, JobFailedEvent."
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for the whole stream.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ],
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl -N",
            "source": "curl -N \\\n  -H \"Authorization: Bearer $OCAI_API_KEY\" \\\n  -H \"X-OCAI-Subject: customer-4711\" \\\n  -H \"Accept: text/event-stream\" \\\n  https://api.open-climate.ai/v1/mapping-jobs/$JOB_ID/events"
          }
        ]
      }
    },
    "/v1/mapping-jobs/{job_id}/result": {
      "get": {
        "tags": [
          "results"
        ],
        "summary": "Get a finished job's statistics",
        "description": "Everything a dashboard needs about a finished job: the totals, the confidence\nsplit, the biggest contributors, and the three ways to take the rows away.\n\n`downloads` holds absolute, ready-to-use URLs for the JSON rows, the CSV and the\nworkbook. Use them as returned rather than rebuilding them from parts.\n\n### Reading `report_stats`\n\n`total_rows` is every data row. `matched_rows` and `unmatched_rows` partition\nit, and `match_rate_pct` is the first number to put on a screen.\n\nTwo of the maps are counts you can sum against something else, which is worth\ndoing as a check in your own code: `confidence_bucket_counts` sums to\n`total_rows`, and `no_match_reason_counts` sums to `unmatched_rows`.\n\n`by_library` and `emissions_by_library` are keyed by the **publisher's own\nlabel** for the dataset a row matched into, for example\n`UK.gov GHG Reporting Factors`, not by the short code you pass in `libraries`.\nOne code can cover several published datasets, so the keys here are the finer\ngrained of the two. The `ef_library` field inside `emissions_by_ef` is the same\npublisher label despite its name. Render all three as labels; do not map them\nback onto your picker.\n\n`top_emitters` is capped at 10 rows and `emissions_by_ef` at 15 factors. Both\nare for display. For anything you intend to compute on, read `/rows`.\n\nThe emissions figures are best-effort: they depend on a quantity and a unit\nbeing resolvable per row, `emissions_column` names the column they were summed\nfrom and can be `null`, and `rows_missing_emissions` counts the rows that\nmatched a factor but could not be turned into a number. A row can be a good\nmatch and still contribute nothing to the total.\n\nCalled before the job succeeds, this answers `409`: `job_not_ready` while it is\nstill running, `job_failed` if it will not produce one.",
        "operationId": "read_result_v1_mapping_jobs__job_id__result_get",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Job Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobResultResponse"
                },
                "example": {
                  "job_id": "e0472d18-9c3b-4a06-b52f-8d71c4e9350a",
                  "client_reference": "cad-2026-03-ledger-04",
                  "status": "succeeded",
                  "row_count": 2,
                  "elapsed_seconds": 11.2,
                  "finished_at": "2026-03-04T09:13:11Z",
                  "downloads": {
                    "rows_json": "https://api.open-climate.ai/v1/mapping-jobs/e0472d18-9c3b-4a06-b52f-8d71c4e9350a/rows",
                    "rows_csv": "https://api.open-climate.ai/v1/mapping-jobs/e0472d18-9c3b-4a06-b52f-8d71c4e9350a/rows.csv",
                    "workbook_xlsx": "https://api.open-climate.ai/v1/mapping-jobs/e0472d18-9c3b-4a06-b52f-8d71c4e9350a/file"
                  },
                  "report_stats": {
                    "total_rows": 2,
                    "matched_rows": 2,
                    "unmatched_rows": 0,
                    "match_rate_pct": 100.0,
                    "by_library": {
                      "Exiobase": 1,
                      "UK.gov GHG Reporting Factors": 1
                    },
                    "emissions_by_library": {
                      "UK.gov GHG Reporting Factors": 502.47,
                      "Exiobase": 46.51
                    },
                    "emissions_column": "final_top_1_total_emissions_kgco2e",
                    "total_emissions_kgco2e": 548.98,
                    "rows_with_emissions": 2,
                    "rows_missing_emissions": 0,
                    "skip_reason_counts": {},
                    "no_match_reason_counts": {},
                    "excluded_used_good_rows": 0,
                    "confidence_bucket_counts": {
                      "high": 1,
                      "acceptable": 1
                    },
                    "top_emitters": [
                      {
                        "row_index": 1,
                        "final_top_1_ef_keyword": "Diesel (average biofuel blend)",
                        "final_top_1_ef_unit": "l",
                        "final_top_1_quantity_in_ef_native_unit": 200.0,
                        "final_top_1_total_emissions_kgco2e": 502.47
                      },
                      {
                        "row_index": 0,
                        "final_top_1_ef_keyword": "Pulp, paper and paper products",
                        "final_top_1_ef_unit": "EUR",
                        "final_top_1_quantity_in_ef_native_unit": 120.5,
                        "final_top_1_total_emissions_kgco2e": 46.51
                      }
                    ],
                    "emissions_by_ef": [
                      {
                        "keyword": "Diesel (average biofuel blend)",
                        "ef_library": "UK.gov GHG Reporting Factors",
                        "ef_unit": "l",
                        "kgco2e": 502.47,
                        "native_qty": 200.0,
                        "row_count": 1
                      },
                      {
                        "keyword": "Pulp, paper and paper products",
                        "ef_library": "Exiobase",
                        "ef_unit": "EUR",
                        "kgco2e": 46.51,
                        "native_qty": 120.5,
                        "row_count": 1
                      }
                    ]
                  },
                  "messages": [
                    {
                      "level": "info",
                      "code": "job_succeeded",
                      "text": "Matched 2 of 2 lines (100.0%), totalling 549 kg CO2e.",
                      "fields": []
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      }
    },
    "/v1/mapping-jobs/{job_id}/file": {
      "get": {
        "tags": [
          "results"
        ],
        "summary": "Download the result workbook",
        "description": "The result workbook, either streamed to you or handed back as a link.\n\n`variant=presentation` (the default) is the formatted workbook, laid out for\nsomeone to open and read. `variant=raw` is every pipeline column, which is what\nyou want when you are diagnosing a match rather than showing it to anyone.\n\n`as=link` returns a short-lived signed URL instead of the bytes. The URL carries\nits own capability token and needs no bearer header, so it can go straight into\na browser that has no way to send one. It expires. Hand it to a user; never\nstore it, and never put it anywhere it would outlive its own lifetime.\n\nWithout `as=link` the bytes stream back with a `Content-Disposition` filename,\nwhich is the right choice for a server-to-server download.",
        "operationId": "download_result_file_v1_mapping_jobs__job_id__file_get",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Job Id"
            }
          },
          {
            "name": "variant",
            "in": "query",
            "required": false,
            "schema": {
              "enum": [
                "presentation",
                "raw"
              ],
              "type": "string",
              "description": "``presentation`` is formatted for humans; ``raw`` is every pipeline column, for debugging.",
              "default": "presentation",
              "title": "Variant"
            },
            "description": "``presentation`` is formatted for humans; ``raw`` is every pipeline column, for debugging."
          },
          {
            "name": "as",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "const": "link",
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "``link`` returns a short-lived signed URL instead of the bytes, for a browser that cannot send the bearer header.",
              "title": "As"
            },
            "description": "``link`` returns a short-lived signed URL instead of the bytes, for a browser that cannot send the bearer header."
          }
        ],
        "responses": {
          "200": {
            "description": "The result workbook, or, with `?as=link`, a short-lived signed URL for it. Which one you get is decided by the query parameter, not by content negotiation.",
            "content": {
              "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DownloadLinkResponse"
                },
                "example": {
                  "download_url": "https://api.open-climate.ai/files/content?job=e0472d18-9c3b-4a06-b52f-8d71c4e9350a&user=7e64d0a9-13cf-4b52-8d70-2f9a6c1e4b03&t=1788177600.EXAMPLE-CAPABILITY-SIGNATURE",
                  "expires_at": "2026-03-04T09:28:11Z",
                  "filename": "ledger_results_e0472d18-9c3b-4a06-b52f-8d71c4e9350a.xlsx"
                }
              }
            },
            "headers": {
              "Content-Disposition": {
                "description": "`attachment` with the result filename, RFC 6266 encoded so a non-ASCII supplier name survives the header.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      }
    },
    "/v1/mapping-jobs/{job_id}/rows": {
      "get": {
        "tags": [
          "rows"
        ],
        "summary": "Read result rows as JSON",
        "description": "One page of the matched rows, each with the factor it matched and the ladder of\ncandidates that were considered.\n\nFiltering happens before pagination, so `filtered_row_count` is the size of the\nwhole filtered set rather than of the page, and `row_index` stays the row's\naddress in the original file across every page and every filter. `next_page_url`\nis absolute and ready to follow; `has_more` tells you when to stop.\n\n`confidence` is repeatable and is what builds a review queue: ask for `review`\nand `review_proxy` and you get exactly the rows a human needs to look at,\nwithout downloading the file. `matched=false` is the complementary view, the\nrows that found nothing.\n\nTwo parameters are about payload size, and on a large ledger they matter more\nthan pagination does. `candidates=0` drops the candidate array entirely, which\nis what a bulk export wants; the default of 3 is what a review UI wants.\n`include_input_columns=false` drops your own columns and roughly halves the\nresponse, which is safe when you still hold the source file and can join on\n`row_index`.\n\n`columns` names the columns in order, so you can build a table without\ninspecting the first row. Row indices are 0-based; message text counts rows from\n1, because that is what the user's spreadsheet shows.",
        "operationId": "read_rows_v1_mapping_jobs__job_id__rows_get",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Job Id"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "description": "1-based page number.",
              "default": 1,
              "title": "Page"
            },
            "description": "1-based page number."
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 2000,
              "minimum": 1,
              "default": 500,
              "title": "Page Size"
            }
          },
          {
            "name": "confidence",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "enum": [
                      "high",
                      "acceptable",
                      "review_proxy",
                      "review",
                      "no_match"
                    ],
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Repeatable. Omit to return every bucket.",
              "title": "Confidence"
            },
            "description": "Repeatable. Omit to return every bucket."
          },
          {
            "name": "matched",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "boolean"
                },
                {
                  "type": "null"
                }
              ],
              "description": "``true`` keeps only rows that matched a factor.",
              "title": "Matched"
            },
            "description": "``true`` keeps only rows that matched a factor."
          },
          {
            "name": "candidates",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 5,
              "minimum": 0,
              "description": "Ranks per row; ``0`` omits them.",
              "default": 3,
              "title": "Candidates"
            },
            "description": "Ranks per row; ``0`` omits them."
          },
          {
            "name": "include_input_columns",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "description": "``false`` drops the customer's own columns.",
              "default": true,
              "title": "Include Input Columns"
            },
            "description": "``false`` drops the customer's own columns."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RowsResponse"
                },
                "example": {
                  "job_id": "e0472d18-9c3b-4a06-b52f-8d71c4e9350a",
                  "filename": "ledger_results_e0472d18-9c3b-4a06-b52f-8d71c4e9350a.xlsx",
                  "row_count": 2,
                  "filtered_row_count": 2,
                  "page": 1,
                  "page_size": 500,
                  "has_more": false,
                  "next_page_url": null,
                  "columns": [
                    "row_index",
                    "Description",
                    "Quantity",
                    "Unit",
                    "Amount",
                    "Currency",
                    "ef_library",
                    "final_top_1_ef_data_source",
                    "final_top_1_ef_fingerprint",
                    "final_top_1_ef_keyword",
                    "final_top_1_ef_attribute",
                    "final_top_1_ef_unit",
                    "final_top_1_ef_value_kgco2e_per_native_unit",
                    "final_top_1_quantity_in_ef_native_unit",
                    "final_top_1_magnitude_applied",
                    "final_top_1_total_emissions_kgco2e",
                    "confidence",
                    "no_match_reason",
                    "review_reason",
                    "basis_fallback",
                    "reformulation_item_nature",
                    "agent_reasoning",
                    "detected_quantities",
                    "quantity_conversion_detail",
                    "candidates_status",
                    "selected_candidate_rank",
                    "candidates"
                  ],
                  "rows": [
                    {
                      "row_index": 0,
                      "Description": "A4 copier paper 80gsm",
                      "Quantity": 10,
                      "Unit": "kg",
                      "Amount": 120.5,
                      "Currency": "EUR",
                      "ef_library": "exiobase",
                      "final_top_1_ef_data_source": "Exiobase",
                      "final_top_1_ef_fingerprint": "exiobase-4c1f8e02",
                      "final_top_1_ef_keyword": "Pulp, paper and paper products",
                      "final_top_1_ef_attribute": "GB | purchaser price",
                      "final_top_1_ef_unit": "EUR",
                      "final_top_1_ef_value_kgco2e_per_native_unit": 0.386,
                      "final_top_1_quantity_in_ef_native_unit": 120.5,
                      "final_top_1_magnitude_applied": 1,
                      "final_top_1_total_emissions_kgco2e": 46.51,
                      "confidence": "acceptable",
                      "no_match_reason": null,
                      "review_reason": null,
                      "basis_fallback": "activity_based_physical_unit->monetary",
                      "reformulation_item_nature": "raw_material",
                      "agent_reasoning": "The line states a mass, but no paper grade specific enough for a process factor, so the match falls back to the spend basis and uses the line amount against the EXIOBASE paper sector.",
                      "detected_quantities": [
                        {
                          "slot": "physical",
                          "quantity": 10.0,
                          "unit_original": "kg",
                          "unit_normalized": "kg",
                          "unit_scale": 1.0,
                          "magnitude_applied": 1.0,
                          "granularity": "total",
                          "is_explicit_zero": false,
                          "source": "bound_column",
                          "source_columns": {
                            "quantity": "Quantity",
                            "unit": "Unit"
                          },
                          "used_for_match": false
                        },
                        {
                          "slot": "monetary",
                          "quantity": 120.5,
                          "unit_original": "EUR",
                          "unit_normalized": "EUR",
                          "unit_scale": 1.0,
                          "magnitude_applied": 1.0,
                          "granularity": "total",
                          "is_explicit_zero": false,
                          "source": "bound_column",
                          "source_columns": {
                            "quantity": "Amount",
                            "unit": "Currency"
                          },
                          "used_for_match": true
                        }
                      ],
                      "quantity_conversion_detail": {
                        "line_quantity": 120.5,
                        "line_unit": "EUR",
                        "ef_native_unit": "EUR",
                        "quantity_in_ef_native_unit": 120.5,
                        "steps": [],
                        "rendered": null
                      },
                      "candidates_status": "ranked",
                      "selected_candidate_rank": 1,
                      "candidates": [
                        {
                          "rank": 1,
                          "selected": true,
                          "ef_fingerprint": "exiobase-4c1f8e02",
                          "ef_keyword": "Pulp, paper and paper products",
                          "ef_attribute": "GB | purchaser price",
                          "ef_unit": "EUR",
                          "ef_data_source": "Exiobase",
                          "ef_value_kgco2e_per_native_unit": 0.386,
                          "quantity_in_ef_native_unit": 120.5,
                          "total_emissions_kgco2e": 46.51,
                          "total_emissions_skip_reason": null,
                          "score": 0.83,
                          "emissions_delta_pct": 0.0
                        },
                        {
                          "rank": 2,
                          "selected": false,
                          "ef_fingerprint": "exiobase-9d20b7a4",
                          "ef_keyword": "Printing and publishing services",
                          "ef_attribute": "GB | purchaser price",
                          "ef_unit": "EUR",
                          "ef_data_source": "Exiobase",
                          "ef_value_kgco2e_per_native_unit": 0.244,
                          "quantity_in_ef_native_unit": 120.5,
                          "total_emissions_kgco2e": 29.4,
                          "total_emissions_skip_reason": null,
                          "score": 0.71,
                          "emissions_delta_pct": -36.8
                        },
                        {
                          "rank": 3,
                          "selected": false,
                          "ef_fingerprint": "exiobase-1a58c630",
                          "ef_keyword": "Wood and products of wood and cork",
                          "ef_attribute": "GB | purchaser price",
                          "ef_unit": "EUR",
                          "ef_data_source": "Exiobase",
                          "ef_value_kgco2e_per_native_unit": 0.412,
                          "quantity_in_ef_native_unit": 120.5,
                          "total_emissions_kgco2e": 49.65,
                          "total_emissions_skip_reason": null,
                          "score": 0.64,
                          "emissions_delta_pct": 6.8
                        }
                      ]
                    },
                    {
                      "row_index": 1,
                      "Description": "Road diesel B7",
                      "Quantity": 200,
                      "Unit": "litre",
                      "Amount": 340.0,
                      "Currency": "EUR",
                      "ef_library": "fuel_combustion",
                      "final_top_1_ef_data_source": "UK.gov GHG Reporting Factors",
                      "final_top_1_ef_fingerprint": "desnz-7b41c0d9",
                      "final_top_1_ef_keyword": "Diesel (average biofuel blend)",
                      "final_top_1_ef_attribute": "Scope 1 | combustion",
                      "final_top_1_ef_unit": "l",
                      "final_top_1_ef_value_kgco2e_per_native_unit": 2.51233,
                      "final_top_1_quantity_in_ef_native_unit": 200.0,
                      "final_top_1_magnitude_applied": 1,
                      "final_top_1_total_emissions_kgco2e": 502.47,
                      "confidence": "high",
                      "no_match_reason": null,
                      "review_reason": null,
                      "basis_fallback": null,
                      "reformulation_item_nature": "fuel",
                      "agent_reasoning": "A combustion fuel with a volume on the line, so the physical basis applies: 200 litres against the DESNZ Scope 1 factor for the average biofuel blend sold at the pump.",
                      "detected_quantities": [
                        {
                          "slot": "physical",
                          "quantity": 200.0,
                          "unit_original": "litre",
                          "unit_normalized": "l",
                          "unit_scale": 1.0,
                          "magnitude_applied": 1.0,
                          "granularity": "total",
                          "is_explicit_zero": false,
                          "source": "bound_column",
                          "source_columns": {
                            "quantity": "Quantity",
                            "unit": "Unit"
                          },
                          "used_for_match": true
                        },
                        {
                          "slot": "monetary",
                          "quantity": 340.0,
                          "unit_original": "EUR",
                          "unit_normalized": "EUR",
                          "unit_scale": 1.0,
                          "magnitude_applied": 1.0,
                          "granularity": "total",
                          "is_explicit_zero": false,
                          "source": "bound_column",
                          "source_columns": {
                            "quantity": "Amount",
                            "unit": "Currency"
                          },
                          "used_for_match": false
                        }
                      ],
                      "quantity_conversion_detail": {
                        "line_quantity": 200.0,
                        "line_unit": "litre",
                        "ef_native_unit": "l",
                        "quantity_in_ef_native_unit": 200.0,
                        "steps": [
                          {
                            "kind": "unit_bridge",
                            "from": "litre",
                            "to": "l",
                            "factor": 1.0,
                            "source": "unit registry",
                            "rendered": "litre is the same unit as l; the quantity is unchanged."
                          }
                        ],
                        "rendered": "litre is the same unit as l; the quantity is unchanged."
                      },
                      "candidates_status": "ranked",
                      "selected_candidate_rank": 1,
                      "candidates": [
                        {
                          "rank": 1,
                          "selected": true,
                          "ef_fingerprint": "desnz-7b41c0d9",
                          "ef_keyword": "Diesel (average biofuel blend)",
                          "ef_attribute": "Scope 1 | combustion",
                          "ef_unit": "l",
                          "ef_data_source": "UK.gov GHG Reporting Factors",
                          "ef_value_kgco2e_per_native_unit": 2.51233,
                          "quantity_in_ef_native_unit": 200.0,
                          "total_emissions_kgco2e": 502.47,
                          "total_emissions_skip_reason": null,
                          "score": 0.94,
                          "emissions_delta_pct": 0.0
                        },
                        {
                          "rank": 2,
                          "selected": false,
                          "ef_fingerprint": "ademe-2f90ab13",
                          "ef_keyword": "Gazole routier B7",
                          "ef_attribute": "combustion | France",
                          "ef_unit": "l",
                          "ef_data_source": "ADEME Base Carbone",
                          "ef_value_kgco2e_per_native_unit": 2.66,
                          "quantity_in_ef_native_unit": 200.0,
                          "total_emissions_kgco2e": 532.0,
                          "total_emissions_skip_reason": null,
                          "score": 0.88,
                          "emissions_delta_pct": 5.9
                        }
                      ]
                    }
                  ],
                  "messages": []
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      }
    },
    "/v1/mapping-jobs/{job_id}/rows.csv": {
      "get": {
        "tags": [
          "rows"
        ],
        "summary": "Download result rows as CSV",
        "description": "The same rows as a spreadsheet, streamed as one file.\n\nTakes the same `confidence`, `matched`, `candidates` and `include_input_columns`\nfilters as the JSON route and ignores pagination entirely: a CSV has no next\npage, so you get the whole filtered set in one response.\n\n### The format\n\nUTF-8 **with a byte-order mark**, comma separated, RFC 4180 quoting. The BOM is\ndeliberate: without it, Excel on Windows reads the file as the local codepage\nand mangles every accented supplier name. Strip it if your own parser does not.\n\nColumn order follows the JSON route's `columns` array, with two differences.\n\n`detected_quantities` and `quantity_conversion_detail` stay nested and become\ncompact JSON strings inside their cells. They are diagnostic, read one row at a\ntime, so keeping the sheet flat is worth more than making them sortable. Someone\nreading the sheet by eye wants `agent_reasoning` instead.\n\nThe candidate ladder is flattened into columns rather than a JSON string,\nbecause it is comparative: sorting the whole sheet by\n`candidate_2_emissions_delta_pct` is how a reviewer finds the lines where the\nsecond choice would have moved the number. `candidates=3` adds a\n`candidate_2_*` and a `candidate_3_*` group, each with the keyword, attribute,\nunit, data source, fingerprint, factor value, converted quantity, emissions and\ndelta. Rank 1 already has its `final_top_1_*` columns and is not repeated.\n\nThose groups are addressed by rank, not by depth. On a row whose first-ranked\nfactor was rejected in the audit the ladder starts at rank 2, so its third\nsurviving alternative is rank 4 and has no column here. The JSON route is\naddressed by depth and still carries it.",
        "operationId": "read_rows_csv_v1_mapping_jobs__job_id__rows_csv_get",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Job Id"
            }
          },
          {
            "name": "confidence",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "enum": [
                      "high",
                      "acceptable",
                      "review_proxy",
                      "review",
                      "no_match"
                    ],
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Repeatable. Omit to return every bucket.",
              "title": "Confidence"
            },
            "description": "Repeatable. Omit to return every bucket."
          },
          {
            "name": "matched",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "boolean"
                },
                {
                  "type": "null"
                }
              ],
              "description": "``true`` keeps only rows that matched a factor.",
              "title": "Matched"
            },
            "description": "``true`` keeps only rows that matched a factor."
          },
          {
            "name": "candidates",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 5,
              "minimum": 0,
              "description": "Ranks per row; ``0`` omits them.",
              "default": 3,
              "title": "Candidates"
            },
            "description": "Ranks per row; ``0`` omits them."
          },
          {
            "name": "include_input_columns",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "description": "``false`` drops the customer's own columns.",
              "default": true,
              "title": "Include Input Columns"
            },
            "description": "``false`` drops the customer's own columns."
          }
        ],
        "responses": {
          "200": {
            "description": "The whole filtered set as a spreadsheet. Same filters as the JSON route; pagination is ignored, because a spreadsheet has no next page.",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "﻿row_index,Description,Quantity,Unit,Amount,Currency,ef_library,final_top_1_ef_data_source,final_top_1_ef_fingerprint,final_top_1_ef_keyword,final_top_1_ef_attribute,final_top_1_ef_unit,final_top_1_ef_value_kgco2e_per_native_unit,final_top_1_quantity_in_ef_native_unit,final_top_1_magnitude_applied,final_top_1_total_emissions_kgco2e,confidence,no_match_reason,review_reason,basis_fallback,reformulation_item_nature,agent_reasoning,detected_quantities,quantity_conversion_detail,candidates_status,selected_candidate_rank,candidate_2_ef_keyword,candidate_2_ef_attribute,candidate_2_ef_unit,candidate_2_ef_data_source,candidate_2_ef_fingerprint,candidate_2_ef_value_kgco2e_per_native_unit,candidate_2_quantity_in_ef_native_unit,candidate_2_total_emissions_kgco2e,candidate_2_emissions_delta_pct,candidate_3_ef_keyword,candidate_3_ef_attribute,candidate_3_ef_unit,candidate_3_ef_data_source,candidate_3_ef_fingerprint,candidate_3_ef_value_kgco2e_per_native_unit,candidate_3_quantity_in_ef_native_unit,candidate_3_total_emissions_kgco2e,candidate_3_emissions_delta_pct\r\n0,A4 copier paper 80gsm,10,kg,120.5,EUR,exiobase,Exiobase,exiobase-4c1f8e02,\"Pulp, paper and paper products\",GB | purchaser price,EUR,0.386,120.5,1,46.51,acceptable,,,activity_based_physical_unit->monetary,raw_material,\"The line states a mass, but no paper grade specific enough for a process factor, so the match falls back to the spend basis and uses the line amount against the EXIOBASE paper sector.\",\"[{\"\"slot\"\":\"\"physical\"\",\"\"quantity\"\":10.0,\"\"unit_original\"\":\"\"kg\"\",\"\"unit_normalized\"\":\"\"kg\"\",\"\"unit_scale\"\":1.0,\"\"magnitude_applied\"\":1.0,\"\"granularity\"\":\"\"total\"\",\"\"is_explicit_zero\"\":false,\"\"source\"\":\"\"bound_column\"\",\"\"source_columns\"\":{\"\"quantity\"\":\"\"Quantity\"\",\"\"unit\"\":\"\"Unit\"\"},\"\"used_for_match\"\":false},{\"\"slot\"\":\"\"monetary\"\",\"\"quantity\"\":120.5,\"\"unit_original\"\":\"\"EUR\"\",\"\"unit_normalized\"\":\"\"EUR\"\",\"\"unit_scale\"\":1.0,\"\"magnitude_applied\"\":1.0,\"\"granularity\"\":\"\"total\"\",\"\"is_explicit_zero\"\":false,\"\"source\"\":\"\"bound_column\"\",\"\"source_columns\"\":{\"\"quantity\"\":\"\"Amount\"\",\"\"unit\"\":\"\"Currency\"\"},\"\"used_for_match\"\":true}]\",\"{\"\"line_quantity\"\":120.5,\"\"line_unit\"\":\"\"EUR\"\",\"\"ef_native_unit\"\":\"\"EUR\"\",\"\"quantity_in_ef_native_unit\"\":120.5,\"\"steps\"\":[],\"\"rendered\"\":null}\",ranked,1,Printing and publishing services,GB | purchaser price,EUR,Exiobase,exiobase-9d20b7a4,0.244,120.5,29.4,-36.8,Wood and products of wood and cork,GB | purchaser price,EUR,Exiobase,exiobase-1a58c630,0.412,120.5,49.65,6.8\r\n1,Road diesel B7,200,litre,340.0,EUR,fuel_combustion,UK.gov GHG Reporting Factors,desnz-7b41c0d9,Diesel (average biofuel blend),Scope 1 | combustion,l,2.51233,200.0,1,502.47,high,,,,fuel,\"A combustion fuel with a volume on the line, so the physical basis applies: 200 litres against the DESNZ Scope 1 factor for the average biofuel blend sold at the pump.\",\"[{\"\"slot\"\":\"\"physical\"\",\"\"quantity\"\":200.0,\"\"unit_original\"\":\"\"litre\"\",\"\"unit_normalized\"\":\"\"l\"\",\"\"unit_scale\"\":1.0,\"\"magnitude_applied\"\":1.0,\"\"granularity\"\":\"\"total\"\",\"\"is_explicit_zero\"\":false,\"\"source\"\":\"\"bound_column\"\",\"\"source_columns\"\":{\"\"quantity\"\":\"\"Quantity\"\",\"\"unit\"\":\"\"Unit\"\"},\"\"used_for_match\"\":true},{\"\"slot\"\":\"\"monetary\"\",\"\"quantity\"\":340.0,\"\"unit_original\"\":\"\"EUR\"\",\"\"unit_normalized\"\":\"\"EUR\"\",\"\"unit_scale\"\":1.0,\"\"magnitude_applied\"\":1.0,\"\"granularity\"\":\"\"total\"\",\"\"is_explicit_zero\"\":false,\"\"source\"\":\"\"bound_column\"\",\"\"source_columns\"\":{\"\"quantity\"\":\"\"Amount\"\",\"\"unit\"\":\"\"Currency\"\"},\"\"used_for_match\"\":false}]\",\"{\"\"line_quantity\"\":200.0,\"\"line_unit\"\":\"\"litre\"\",\"\"ef_native_unit\"\":\"\"l\"\",\"\"quantity_in_ef_native_unit\"\":200.0,\"\"steps\"\":[{\"\"kind\"\":\"\"unit_bridge\"\",\"\"from\"\":\"\"litre\"\",\"\"to\"\":\"\"l\"\",\"\"factor\"\":1.0,\"\"source\"\":\"\"unit registry\"\",\"\"rendered\"\":\"\"litre is the same unit as l; the quantity is unchanged.\"\"}],\"\"rendered\"\":\"\"litre is the same unit as l; the quantity is unchanged.\"\"}\",ranked,1,Gazole routier B7,combustion | France,l,ADEME Base Carbone,ademe-2f90ab13,2.66,200.0,532.0,5.9,,,,,,,,,\r\n"
              }
            },
            "headers": {
              "Content-Disposition": {
                "description": "`attachment` with the result filename, RFC 6266 encoded so a non-ASCII supplier name survives the header.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      }
    },
    "/v1/exports/slice": {
      "get": {
        "tags": [
          "exports"
        ],
        "summary": "Download the current slice synchronously",
        "description": "Stream the filtered slice as CSV, or build it as XLSX, within caps.\n\nOver the cap the answer is a 400 naming ``POST /v1/exports/jobs`` — the\nasync path exists exactly for those slices. ``principal`` gates the\nroute; the licence mapping, not the caller, decides row visibility.",
        "operationId": "export_slice_v1_exports_slice_get",
        "parameters": [
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Library codes (publisher codes) to include.",
              "title": "Library"
            },
            "description": "Library codes (publisher codes) to include."
          },
          {
            "name": "sector",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Top-level activity sectors to include.",
              "title": "Sector"
            },
            "description": "Top-level activity sectors to include."
          },
          {
            "name": "region",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Region codes (ISO-like geography codes).",
              "title": "Region"
            },
            "description": "Region codes (ISO-like geography codes)."
          },
          {
            "name": "unit",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Denominator unit codes, e.g. kg, kWh, EUR2022.",
              "title": "Unit"
            },
            "description": "Denominator unit codes, e.g. kg, kWh, EUR2022."
          },
          {
            "name": "unit_family",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Unit families, e.g. mass, energy, monetary.",
              "title": "Unit Family"
            },
            "description": "Unit families, e.g. mass, energy, monetary."
          },
          {
            "name": "boundary",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "System-boundary families, e.g. cradle-to-gate.",
              "title": "Boundary"
            },
            "description": "System-boundary families, e.g. cradle-to-gate."
          },
          {
            "name": "gwp_method",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "GWP methodologies, e.g. AR5, AR6.",
              "title": "Gwp Method"
            },
            "description": "GWP methodologies, e.g. AR5, AR6."
          },
          {
            "name": "license_tier",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "enum": [
                      "open",
                      "copyleft",
                      "restricted",
                      "prohibited"
                    ],
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Licence tiers to include. Tiers the caller may not export are excluded from the file regardless and counted out loud.",
              "title": "License Tier"
            },
            "description": "Licence tiers to include. Tiers the caller may not export are excluded from the file regardless and counted out loud."
          },
          {
            "name": "confidence_tier",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Cross-library confidence tiers to include.",
              "title": "Confidence Tier"
            },
            "description": "Cross-library confidence tiers to include."
          },
          {
            "name": "year",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "integer"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Reference years to include.",
              "title": "Year"
            },
            "description": "Reference years to include."
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "enum": [
                "csv",
                "xlsx"
              ],
              "type": "string",
              "description": "Artifact format: csv (default) or xlsx.",
              "default": "csv",
              "title": "Format"
            },
            "description": "Artifact format: csv (default) or xlsx."
          },
          {
            "name": "columns",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Column-registry ids, rendered in this order. Omitted means the default-visible set plus license_code and attribution_text.",
              "title": "Columns"
            },
            "description": "Column-registry ids, rendered in this order. Omitted means the default-visible set plus license_code and attribution_text."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      }
    },
    "/v1/exports/jobs": {
      "post": {
        "tags": [
          "exports"
        ],
        "summary": "Queue an asynchronous export",
        "description": "Record the export and return the handle to poll it on.",
        "operationId": "create_export_job_v1_exports_jobs_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExportSelection"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateExportJobResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      }
    },
    "/v1/exports/jobs/{job_id}": {
      "get": {
        "tags": [
          "exports"
        ],
        "summary": "Poll an export job",
        "description": "The job's status under the caller's scope; cross-principal ids 404.",
        "operationId": "get_export_job_v1_exports_jobs__job_id__get",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Job Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportJobStatusResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      }
    },
    "/v1/exports/editions/{library}/{filename}": {
      "get": {
        "tags": [
          "exports"
        ],
        "summary": "Issue a download URL for a published edition workbook",
        "description": "A presigned GET for one published edition artifact.\n\n``filename`` is exactly what the anonymous library listing carries in a\nworkbook entry's ``filename`` field (the XLSX or the CSV bundle).",
        "operationId": "download_edition_artifact_v1_exports_editions__library___filename__get",
        "parameters": [
          {
            "name": "library",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Library"
            }
          },
          {
            "name": "filename",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Filename"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EditionDownloadResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": [
          {
            "partnerApiKey": [],
            "subjectHeader": []
          },
          {
            "userApiKey": []
          },
          {
            "oauth2": []
          }
        ]
      }
    },
    "/factors/api/factors": {
      "get": {
        "tags": [
          "factors"
        ],
        "summary": "Browse the factor catalogue",
        "description": "Browse the emission-factor catalogue. No credential is needed. Filter with the\nfacet parameters (repeat a parameter to OR within a facet; facets AND\ntogether), or search with `q` for a semantic ranking.\n\nThe two modes answer differently. A filter-only selection returns rows plus\n`total` and `facets` — one block per facet, each option carrying the count the\ncatalogue would show if you applied it. Counts are computed against your\nselection minus that facet's own filter, so narrowing never dead-ends: an\noption with a non-zero count always lands on results, and a zero-count option\nis reported rather than hidden so a UI can disable it in place. A searched\n(`q`) selection returns a ranked top-N with your filters applied and carries\nno `total` and no `facets`.\n\nRows are slim on purpose: identity fields plus the columns you asked for with\n`columns` (or the default set, echoed back in `columns` on the response).\nEverything else lives on the detail route. A row with `value_withheld` true\nbelongs to a licence-restricted library: its metadata is served, its numbers\nare stripped server-side, and following its slug explains what an account\nchanges.\n\nAnonymous browsing is metered: a session gets **ten searches**. A search, for\nthe meter, is a change of filter signature — the query text, the facet\nselection or the library set. Paging with `cursor`, changing `sort` or\n`columns`, and repeating a signature you already spent do not count. Past ten,\nthe route still answers 200, but with the `sign_in_required` envelope instead\nof rows: it names the total match count and the free account that lifts the\nlimit. Branch on the `kind` field to tell the two bodies apart. The detail and\nlibrary routes are never metered.",
        "operationId": "list_factors_factors_api_factors_get",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 500
                },
                {
                  "type": "null"
                }
              ],
              "description": "Free-text semantic query. A searched response is a ranked top-N with post-filters applied and carries no total and no facet counts; a filter-only selection carries both.",
              "title": "Q"
            },
            "description": "Free-text semantic query. A searched response is a ranked top-N with post-filters applied and carries no total and no facet counts; a filter-only selection carries both."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Library codes (publisher codes) to include.",
              "title": "Library"
            },
            "description": "Library codes (publisher codes) to include."
          },
          {
            "name": "sector",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Top-level activity sectors to include.",
              "title": "Sector"
            },
            "description": "Top-level activity sectors to include."
          },
          {
            "name": "region",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Region codes (ISO-like geography codes).",
              "title": "Region"
            },
            "description": "Region codes (ISO-like geography codes)."
          },
          {
            "name": "unit",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Denominator unit codes, e.g. kg, kWh, EUR2022.",
              "title": "Unit"
            },
            "description": "Denominator unit codes, e.g. kg, kWh, EUR2022."
          },
          {
            "name": "unit_family",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Unit families, e.g. mass, energy, monetary.",
              "title": "Unit Family"
            },
            "description": "Unit families, e.g. mass, energy, monetary."
          },
          {
            "name": "boundary",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "System-boundary families, e.g. cradle-to-gate.",
              "title": "Boundary"
            },
            "description": "System-boundary families, e.g. cradle-to-gate."
          },
          {
            "name": "gwp_method",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "GWP methodologies, e.g. AR5, AR6.",
              "title": "Gwp Method"
            },
            "description": "GWP methodologies, e.g. AR5, AR6."
          },
          {
            "name": "license_tier",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "enum": [
                      "open",
                      "copyleft",
                      "restricted",
                      "prohibited"
                    ],
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Licence tiers to include.",
              "title": "License Tier"
            },
            "description": "Licence tiers to include."
          },
          {
            "name": "confidence_tier",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Cross-library confidence tiers to include.",
              "title": "Confidence Tier"
            },
            "description": "Cross-library confidence tiers to include."
          },
          {
            "name": "year",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "integer"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Reference years to include.",
              "title": "Year"
            },
            "description": "Reference years to include."
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "enum": [
                    "relevance",
                    "activity",
                    "-activity",
                    "value",
                    "-value",
                    "year",
                    "-year"
                  ],
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Sort order. Defaults to relevance with a query, activity without.",
              "title": "Sort"
            },
            "description": "Sort order. Defaults to relevance with a query, activity without."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Opaque pagination cursor from a previous response's next_cursor. Paging never counts against the anonymous search meter.",
              "title": "Cursor"
            },
            "description": "Opaque pagination cursor from a previous response's next_cursor. Paging never counts against the anonymous search meter."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 200,
              "minimum": 1,
              "description": "Rows per page, 1 to 200.",
              "default": 50,
              "title": "Limit"
            },
            "description": "Rows per page, 1 to 200."
          },
          {
            "name": "columns",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Column-registry ids to populate on each row beyond the identity fields. Omitted means the registry's default-visible set. Column changes never count against the anonymous search meter.",
              "title": "Columns"
            },
            "description": "Column-registry ids to populate on each row beyond the identity fields. Omitted means the registry's default-visible set. Column changes never count against the anonymous search meter."
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Preferred label language (BCP 47, e.g. fr). Falls back to English per field; machine-translated text is flagged by the serving layer.",
              "title": "Lang"
            },
            "description": "Preferred label language (BCP 47, e.g. fr). Falls back to English per field; machine-translated text is flagged by the serving layer."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/FactorListPage"
                    },
                    {
                      "$ref": "#/components/schemas/SignInRequiredEnvelope"
                    }
                  ],
                  "discriminator": {
                    "propertyName": "kind",
                    "mapping": {
                      "factors": "#/components/schemas/FactorListPage",
                      "sign_in_required": "#/components/schemas/SignInRequiredEnvelope"
                    }
                  },
                  "title": "Response List Factors Factors Api Factors Get"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/factors/api/factors/{slug}": {
      "get": {
        "tags": [
          "factors"
        ],
        "summary": "Read one factor",
        "description": "One factor, whole: activity, geography, unit, system boundary, provenance,\nlicence, its decomposition into component factors, and its year-by-methodology\nmatrix. No credential is needed.\n\nThe matrix is where depth applies. Anonymously you get its shape — how many\nyear-and-methodology combinations exist, the year range, the methodology\nnames — with `values` null; a signed-in reader gets the cells too. The\nresponse says which rendering you received in `depth`, so a client never has\nto guess. `value_withheld` is a different axis entirely: on a\nlicence-restricted library every numeric value is stripped server-side at any\ndepth, and the flag says so explicitly.\n\nSlugs are stable, and survive their factor moving: a request for a former slug\nanswers `301` with the canonical URL in `Location` — follow it and update\nstored links. A slug that never existed answers `404` with code\n`factor_not_found`.",
        "operationId": "read_factor_factors_api_factors__slug__get",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Slug"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Preferred label language (BCP 47); English otherwise.",
              "title": "Lang"
            },
            "description": "Preferred label language (BCP 47); English otherwise."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FactorDetail"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "301": {
            "description": "The slug is a former identifier of a factor that has moved. Location carries the canonical URL; follow it.",
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/factors/api/libraries": {
      "get": {
        "tags": [
          "factors"
        ],
        "summary": "List the catalogue's libraries",
        "description": "The libraries behind the catalogue, one row each: publisher, edition, licence\ntier, attribution line, and coverage — how many factors are served, how many\nof those show values (fewer on a licence-restricted library, where values are\nwithheld), and the reference-year range.\n\nRead `year_semantics` before comparing years across libraries: publishers mean\ndifferent things by a factor's year — publication year, underlying-data year\nor validity start — and this field states which one applies.\n\n`workbooks` lists downloadable exports per library. It is published now so the\nshape is stable, and it stays empty until the export pipeline ships; when it\nfills in, downloads will ask for a free account, as `requires_account` on each\nentry already says. No credential is needed for this route, and it never\ncounts against the anonymous search meter.",
        "operationId": "list_factor_libraries_factors_api_libraries_get",
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Preferred label language (BCP 47); English otherwise.",
              "title": "Lang"
            },
            "description": "Preferred label language (BCP 47); English otherwise."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicLibraryList"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "4XX": {
            "description": "The request was rejected. The body is the standard error envelope; branch on `error.code` and ignore codes you do not recognise. A resource that belongs to another `X-OCAI-Subject` answers `404`, never `403`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          },
          "5XX": {
            "description": "The request failed on our side. `service_unavailable` (503) means the process is still starting and clears on its own, so retry it with backoff; `internal_error` (500) is worth reporting, with the `request_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Correlation handle for this request, minted per request and written to our logs. Present on every response, successful ones included. Quote it in any report you send us.",
                "schema": {
                  "type": "string",
                  "example": "01JZ8QH4M2T7N0S4G6VQK8XBRD"
                }
              }
            }
          }
        },
        "security": []
      }
    }
  },
  "components": {
    "schemas": {
      "Body_create_upload_v1_uploads_post": {
        "properties": {
          "file": {
            "type": "string",
            "contentMediaType": "application/octet-stream",
            "title": "File",
            "description": "CSV or Excel workbook."
          }
        },
        "type": "object",
        "required": [
          "file"
        ],
        "title": "Body_create_upload_v1_uploads_post"
      },
      "Clarification": {
        "properties": {
          "required": {
            "type": "boolean",
            "title": "Required",
            "description": "Whether at least one question needs a human answer.",
            "default": false
          },
          "questions": {
            "items": {
              "$ref": "#/components/schemas/ClarificationQuestion"
            },
            "type": "array",
            "title": "Questions"
          }
        },
        "type": "object",
        "title": "Clarification",
        "description": "The clarification block. ``required`` is ``false`` on the happy path.\n\nA caller may ignore clarifications entirely and post the job anyway; the\nserver then uses its own best guess."
      },
      "ClarificationOption": {
        "properties": {
          "value": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Value",
            "description": "The value to send back in the named request field."
          },
          "label": {
            "type": "string",
            "title": "Label",
            "description": "Human-readable rendering of the option."
          }
        },
        "type": "object",
        "required": [
          "value",
          "label"
        ],
        "title": "ClarificationOption",
        "description": "One selectable answer to a clarification question."
      },
      "ClarificationQuestion": {
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "sheet_ambiguous",
              "header_row_ambiguous",
              "file_currency_unknown",
              "buyer_country_unknown",
              "measure_pairs_unconfirmed"
            ],
            "title": "Code",
            "description": "Stable machine identifier."
          },
          "field": {
            "type": "string",
            "title": "Field",
            "description": "Dotted request-field path the answer belongs in."
          },
          "text": {
            "type": "string",
            "title": "Text",
            "description": "The question, in end-user English."
          },
          "options": {
            "items": {
              "$ref": "#/components/schemas/ClarificationOption"
            },
            "type": "array",
            "title": "Options",
            "description": "Concrete choices. Empty when the answer is free-form."
          }
        },
        "type": "object",
        "required": [
          "code",
          "field",
          "text"
        ],
        "title": "ClarificationQuestion",
        "description": "A question with concrete options, answered by editing one field."
      },
      "CreateExportJobResponse": {
        "properties": {
          "job_id": {
            "type": "string",
            "format": "uuid",
            "title": "Job Id"
          },
          "status": {
            "type": "string",
            "enum": [
              "submitted",
              "processing",
              "available",
              "failed",
              "expired"
            ],
            "title": "Status"
          },
          "status_url": {
            "type": "string",
            "title": "Status Url"
          }
        },
        "type": "object",
        "required": [
          "job_id",
          "status",
          "status_url"
        ],
        "title": "CreateExportJobResponse",
        "description": "The 202 answer to ``POST /v1/exports/jobs``."
      },
      "CreateMappingJobRequest": {
        "properties": {
          "upload_id": {
            "type": "string",
            "format": "uuid",
            "title": "Upload Id",
            "description": "From step 1. Never a path or a URL."
          },
          "sheet_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sheet Name",
            "description": "Worksheet to read (XLSX only)."
          },
          "header_row": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Header Row",
            "description": "0-based header row index."
          },
          "skip_trailing_rows": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Skip Trailing Rows",
            "description": "Trailing rows to drop (a totals block)."
          },
          "useful_columns": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Useful Columns",
            "description": "Whitelist of columns folded into the search query. Dropping a column the matcher needs degrades every row."
          },
          "file_context": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/FileContextInput"
              },
              {
                "type": "null"
              }
            ],
            "description": "Inspect's ``file_context``, flattened and edited by the user. Omit it entirely to have the server infer it."
          },
          "libraries": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Libraries",
            "description": "Publisher allow-list from ``GET /v1/libraries``. Omit it to search every library. An empty list is rejected — it is not a way to say \"none\", and searching nothing would return nothing."
          },
          "client_reference": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 128
              },
              {
                "type": "null"
              }
            ],
            "title": "Client Reference",
            "description": "Opaque correlation handle, echoed on every job payload and progress event."
          }
        },
        "type": "object",
        "required": [
          "upload_id"
        ],
        "title": "CreateMappingJobRequest",
        "description": "Body of ``POST /v1/mapping-jobs`` (spec §4.5).\n\nEvery plan field is optional; ``upload_id`` alone is a valid request and\nlets the server inspect the file and use its own best guess."
      },
      "CreateMappingJobResponse": {
        "properties": {
          "job_id": {
            "type": "string",
            "format": "uuid",
            "title": "Job Id"
          },
          "client_reference": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Client Reference"
          },
          "status": {
            "type": "string",
            "const": "queued",
            "title": "Status",
            "default": "queued"
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "title": "Submitted At"
          },
          "queue_position": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Queue Position",
            "description": "Jobs ahead of this one; 0 means next."
          },
          "total_rows_estimate": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Total Rows Estimate",
            "description": "Data rows counted at submission, before the header and any skipped trailing rows. ``null`` when the file could not be profiled cheaply."
          },
          "events_url": {
            "type": "string",
            "title": "Events Url",
            "description": "Absolute URL of the §4.6 SSE stream."
          },
          "libraries_applied": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Libraries Applied",
            "description": "The publisher codes the job will actually search, with the default expanded — so an omitted ``libraries`` echoes the whole catalogue rather than an empty list."
          },
          "messages": {
            "items": {
              "$ref": "#/components/schemas/Message"
            },
            "type": "array",
            "title": "Messages"
          }
        },
        "type": "object",
        "required": [
          "job_id",
          "submitted_at",
          "events_url"
        ],
        "title": "CreateMappingJobResponse",
        "description": "``202`` body of ``POST /v1/mapping-jobs`` (spec §4.5)."
      },
      "DomainValue": {
        "properties": {
          "value": {
            "type": "string",
            "title": "Value",
            "description": "One of the closed domain vocab (``plastics_compounding``, ``food_processing``, …, ``mixed``, ``unknown``)."
          },
          "confidence": {
            "type": "number",
            "maximum": 1.0,
            "minimum": 0.0,
            "title": "Confidence"
          }
        },
        "type": "object",
        "required": [
          "value",
          "confidence"
        ],
        "title": "DomainValue",
        "description": "File-level domain classification (spec §4)."
      },
      "EditionDownloadResponse": {
        "properties": {
          "library": {
            "type": "string",
            "title": "Library"
          },
          "edition": {
            "type": "string",
            "title": "Edition"
          },
          "revision": {
            "type": "integer",
            "title": "Revision"
          },
          "filename": {
            "type": "string",
            "title": "Filename"
          },
          "sha256": {
            "type": "string",
            "title": "Sha256"
          },
          "byte_size": {
            "type": "integer",
            "title": "Byte Size"
          },
          "expires_in_seconds": {
            "type": "integer",
            "title": "Expires In Seconds"
          },
          "download_url": {
            "type": "string",
            "title": "Download Url"
          }
        },
        "type": "object",
        "required": [
          "library",
          "edition",
          "revision",
          "filename",
          "sha256",
          "byte_size",
          "expires_in_seconds",
          "download_url"
        ],
        "title": "EditionDownloadResponse",
        "description": "The answer to ``GET /v1/exports/editions/{library}/{filename}``.\n\n``download_url`` is a presigned S3 GET valid for ``expires_in_seconds``.\n``sha256`` repeats the manifest checksum from the anonymous listing so\nthe caller can verify the bytes it fetches."
      },
      "EfEmissionsRow": {
        "properties": {
          "keyword": {
            "type": "string",
            "title": "Keyword",
            "description": "Display label for the EF — the ``final_top_1_ef_keyword`` value shared by every matched row in the group."
          },
          "ef_library": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ef Library",
            "description": "Publishing library the matched factor came from — the result file's ``final_top_1_ef_data_source`` (e.g. ``Ecoinvent``, ``ADEME Base Carbone``). NOT the route: a multi-source route such as ``fuel_combustion`` spans several publishers."
          },
          "ef_unit": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Ef Unit",
            "description": "Native unit of the emission factor (e.g. ``kg``, ``EUR``, ``MJ``). ``None`` when the result file has no ``final_top_1_ef_unit`` column or every matching row was blank."
          },
          "kgco2e": {
            "type": "number",
            "title": "Kgco2E",
            "description": "Sum of per-row emissions across the group, in kg CO2e."
          },
          "native_qty": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Native Qty",
            "description": "Sum of ``final_top_1_quantity_in_ef_native_unit`` across the group, in ``ef_unit``. ``None`` when the column was absent or every value was NaN."
          },
          "row_count": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Row Count",
            "description": "Number of matched rows mapped to this EF in the file."
          }
        },
        "type": "object",
        "required": [
          "keyword",
          "kgco2e",
          "row_count"
        ],
        "title": "EfEmissionsRow",
        "description": "One aggregated row for the \"Top emissions per emission factor\" chart.\n\nComputed by grouping the result file on ``final_top_1_ef_keyword`` and\nsumming per-row emissions + native-unit quantities. Each EF carries a\nsingle native unit (``final_top_1_ef_unit``) — different EFs may use\ndifferent units, so the widget axis is kgCO2e while the per-bar\nsub-label shows the native quantity with its unit."
      },
      "EmissionReportStats": {
        "properties": {
          "total_rows": {
            "type": "integer",
            "title": "Total Rows",
            "description": "Total rows in the result file."
          },
          "matched_rows": {
            "type": "integer",
            "title": "Matched Rows",
            "description": "Rows with a successful emission-factor match (final_found is true)."
          },
          "unmatched_rows": {
            "type": "integer",
            "title": "Unmatched Rows",
            "description": "Rows with no emission-factor match."
          },
          "match_rate_pct": {
            "type": "number",
            "maximum": 100.0,
            "minimum": 0.0,
            "title": "Match Rate Pct",
            "description": "100 * matched_rows / max(total_rows, 1), rounded to 1 decimal."
          },
          "by_library": {
            "additionalProperties": {
              "type": "integer"
            },
            "type": "object",
            "title": "By Library",
            "description": "Count of matched rows per publishing library (``final_top_1_ef_data_source``), descending by count. Falls back to the ``ef_library`` route slug only for result files written before the publisher column existed."
          },
          "emissions_by_library": {
            "additionalProperties": {
              "type": "number"
            },
            "type": "object",
            "title": "Emissions By Library",
            "description": "Sum of ``emissions_column`` per publishing library in kg CO2e, descending. Empty when no emissions column is detected. Keys match those of ``by_library``; values are floats (NaN dropped)."
          },
          "emissions_column": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Emissions Column",
            "description": "Name of the column the server interpreted as the per-row emission total (kg CO2e). For pipeline outputs this is normally ``total_emissions_kgco2e``; otherwise it falls back to a recognisable emissions column in the user's input. ``None`` when no such column was detected."
          },
          "total_emissions_kgco2e": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Total Emissions Kgco2E",
            "description": "Sum of ``emissions_column`` across all rows in kg CO2e. ``None`` when no emissions column is detected."
          },
          "rows_with_emissions": {
            "type": "integer",
            "title": "Rows With Emissions",
            "description": "Number of rows for which an emission value could be computed.",
            "default": 0
          },
          "rows_missing_emissions": {
            "type": "integer",
            "title": "Rows Missing Emissions",
            "description": "Number of rows whose emission value is missing or NaN (no match, missing quantity, or pipeline skip).",
            "default": 0
          },
          "skip_reason_counts": {
            "additionalProperties": {
              "type": "integer"
            },
            "type": "object",
            "title": "Skip Reason Counts",
            "description": "Counts of pipeline ``total_emissions_skip_reason`` values across the result file. Empty when the file has no such column. Useful to explain why some emissions could not be computed."
          },
          "no_match_reason_counts": {
            "additionalProperties": {
              "type": "integer"
            },
            "type": "object",
            "title": "No Match Reason Counts",
            "description": "Counts of pipeline ``no_match_reason`` values across the unmatched rows. ``routing_empty`` means the line was not routed to any library (skipped); ``library_filtered_out`` means the job's ``libraries`` selection removed every publisher that could serve the line — it would have matched without the filter, so it is a property of the request, not of the line; ``retrieval_empty`` / ``reranker_rejected`` mean a library was searched but no acceptable factor was found. The counts sum to ``unmatched_rows``; empty when the LLM reranker is disabled (column absent)."
          },
          "excluded_used_good_rows": {
            "type": "integer",
            "title": "Excluded Used Good Rows",
            "description": "Rows the classifier flagged as second-hand / used goods (``reformulation_item_nature == 'excluded_used_good'``). Their cradle-to-gate emissions stayed with the first owner under ecoinvent cutoff, so the line is intentionally not matched. These are a subset of the ``routing_empty`` unmatched rows; surfaced separately so the dashboard can show them on their own line. ``0`` when the classifier column is absent.",
            "default": 0
          },
          "confidence_bucket_counts": {
            "additionalProperties": {
              "type": "integer"
            },
            "type": "object",
            "title": "Confidence Bucket Counts",
            "description": "Rows bucketed by reranker ``confidence``: the accepted-match labels ``high`` / ``acceptable`` / ``review_proxy`` / ``review`` (ordered best → worst), then a trailing ``no_match`` bucket for every other row (rejected + blank / unscored). The counts sum to ``total_rows``. Empty when the LLM reranker is disabled (column absent)."
          },
          "top_emitters": {
            "items": {
              "additionalProperties": true,
              "type": "object"
            },
            "type": "array",
            "title": "Top Emitters",
            "description": "Top 10 rows by ``emissions_column`` value (descending). Each row contains identifying label columns (``search_query``, ``final_top_1_ef_keyword``), unit + quantity columns when available (``final_top_1_ef_unit``, ``final_top_1_quantity_in_ef_native_unit``), and the emissions value. Empty when no emissions column is detected."
          },
          "emissions_by_ef": {
            "items": {
              "$ref": "#/components/schemas/EfEmissionsRow"
            },
            "type": "array",
            "title": "Emissions By Ef",
            "description": "Top emissions per emission factor — matched rows grouped by ``final_top_1_ef_keyword``, summed in kg CO2e (descending). Each entry also carries the EF's native unit and the summed native quantity so the widget can render a sub-label like ``Beef, fresh · 1,200 kg``. Capped at the top 15 EFs by emissions; empty when no emissions column or EF keyword column is detected."
          }
        },
        "type": "object",
        "required": [
          "total_rows",
          "matched_rows",
          "unmatched_rows",
          "match_rate_pct"
        ],
        "title": "EmissionReportStats",
        "description": "Summary statistics computed from a succeeded mapping job's output file.\n\nComputed best-effort by the server. Emission-related fields\n(``emissions_column``, ``total_emissions_kgco2e``, ``top_emitters``,\n``rows_with_emissions``) are populated only when the result file contains\na recognisable emissions column — normally the pipeline's\n``total_emissions_kgco2e`` column, with ``co2e`` / ``kg_co2e`` / etc. as\nfallbacks. Otherwise they are ``None`` / empty and the client should\nreport match stats only."
      },
      "ExportJobStatusResponse": {
        "properties": {
          "job_id": {
            "type": "string",
            "format": "uuid",
            "title": "Job Id"
          },
          "status": {
            "type": "string",
            "enum": [
              "submitted",
              "processing",
              "available",
              "failed",
              "expired"
            ],
            "title": "Status"
          },
          "format": {
            "type": "string",
            "enum": [
              "csv",
              "xlsx"
            ],
            "title": "Format"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "started_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Started At"
          },
          "finished_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Finished At"
          },
          "expires_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "processed_rows": {
            "type": "integer",
            "title": "Processed Rows"
          },
          "total_rows": {
            "type": "integer",
            "title": "Total Rows"
          },
          "excluded_row_count": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Excluded Row Count"
          },
          "artifact_bytes": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Artifact Bytes"
          },
          "download_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Download Url"
          },
          "error_message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error Message"
          }
        },
        "type": "object",
        "required": [
          "job_id",
          "status",
          "format",
          "created_at",
          "started_at",
          "finished_at",
          "expires_at",
          "processed_rows",
          "total_rows",
          "excluded_row_count",
          "artifact_bytes",
          "download_url",
          "error_message"
        ],
        "title": "ExportJobStatusResponse",
        "description": "The answer to ``GET /v1/exports/jobs/{job_id}``.\n\n``download_url`` is a presigned S3 GET, present only while the job is\n``available`` and inside its TTL. ``error_message`` is a fixed sanitized\nsentence — raw exception text stays in the DB for operators."
      },
      "ExportSelection": {
        "properties": {
          "library": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Library",
            "description": "Library codes (publisher codes) to include."
          },
          "sector": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sector",
            "description": "Top-level activity sectors to include."
          },
          "region": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Region",
            "description": "Region codes (ISO-like geography codes)."
          },
          "unit": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Unit",
            "description": "Denominator unit codes, e.g. kg, kWh, EUR2022."
          },
          "unit_family": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Unit Family",
            "description": "Unit families, e.g. mass, energy, monetary."
          },
          "boundary": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Boundary",
            "description": "System-boundary families, e.g. cradle-to-gate."
          },
          "gwp_method": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Gwp Method",
            "description": "GWP methodologies, e.g. AR5, AR6."
          },
          "license_tier": {
            "anyOf": [
              {
                "items": {
                  "type": "string",
                  "enum": [
                    "open",
                    "copyleft",
                    "restricted",
                    "prohibited"
                  ]
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "License Tier",
            "description": "Licence tiers to include. Tiers the caller may not export are excluded from the file regardless and counted out loud."
          },
          "confidence_tier": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Confidence Tier",
            "description": "Cross-library confidence tiers to include."
          },
          "year": {
            "anyOf": [
              {
                "items": {
                  "type": "integer"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Year",
            "description": "Reference years to include."
          },
          "format": {
            "type": "string",
            "enum": [
              "csv",
              "xlsx"
            ],
            "title": "Format",
            "description": "Artifact format: csv (default) or xlsx.",
            "default": "csv"
          },
          "columns": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Columns",
            "description": "Column-registry ids, rendered in this order. Omitted means the default-visible set plus license_code and attribution_text."
          }
        },
        "type": "object",
        "title": "ExportSelection",
        "description": "The parameters of one export: facet filters, format and columns.\n\nThe ten facet fields mirror the factor list route's\n:class:`~open_climate_ai.api.public.schemas_query.FactorListSelection`\nfacets one for one — OR within a facet, AND across facets — so the portal\ncan turn a browse screen into an export without translating anything.\nDoubles as the slice route's query model and the job route's body."
      },
      "FacetBlock": {
        "properties": {
          "facet": {
            "type": "string",
            "title": "Facet",
            "description": "Facet id — identical to the query parameter of the same name (library, sector, region, unit, unit_family, boundary, gwp_method, license_tier, confidence_tier, year)."
          },
          "options": {
            "items": {
              "$ref": "#/components/schemas/FacetOption"
            },
            "type": "array",
            "title": "Options",
            "description": "Options with counts, in the serving layer's stable order."
          }
        },
        "type": "object",
        "required": [
          "facet",
          "options"
        ],
        "title": "FacetBlock",
        "description": "The options of one facet under the current selection.\n\nNever-dead-end semantics: each facet's counts are computed under the\ncurrent selection minus that facet's own filter, so an already-selected\nfacet still shows what the alternatives would yield, and choosing an\noption with a non-zero count never lands on an empty result."
      },
      "FacetOption": {
        "properties": {
          "value": {
            "type": "string",
            "title": "Value",
            "description": "The filter value to send back as-is."
          },
          "label": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Label",
            "description": "Display label in the resolved language."
          },
          "count": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Count",
            "description": "Factors matched when this option is applied to the current selection. A zero renders disabled, never hidden."
          }
        },
        "type": "object",
        "required": [
          "value",
          "count"
        ],
        "title": "FacetOption",
        "description": "One selectable value of a facet, with its match count."
      },
      "FactorDetail": {
        "properties": {
          "slug": {
            "type": "string",
            "title": "Slug",
            "description": "Canonical public identifier."
          },
          "library": {
            "type": "string",
            "title": "Library",
            "description": "Library (publisher) code."
          },
          "source_key": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Source Key",
            "description": "The publisher's own identifier, when stable."
          },
          "activity_full": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Activity Full",
            "description": "Full activity label, in the resolved language."
          },
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Description",
            "description": "Publisher prose."
          },
          "usage_definition": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Usage Definition",
            "description": "When to use this factor, per the publisher."
          },
          "sector": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sector",
            "description": "Top activity level."
          },
          "sub_sector": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sub Sector",
            "description": "Leaf activity level."
          },
          "classifications": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/JsonValue"
              },
              {
                "type": "null"
              }
            ],
            "description": "Classification-scheme paths, publisher-shaped JSON."
          },
          "region_code": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Region Code",
            "description": "Geography code."
          },
          "unit_code": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Unit Code",
            "description": "Denominator unit."
          },
          "unit_family": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Unit Family",
            "description": "Unit family."
          },
          "system_boundary": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "System Boundary",
            "description": "System boundary name."
          },
          "boundary_family": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Boundary Family",
            "description": "Cross-library boundary family."
          },
          "boundary_profile": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/JsonValue"
              },
              {
                "type": "null"
              }
            ],
            "description": "Per-scope-question boundary answers, publisher-shaped JSON."
          },
          "parent_slug": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Parent Slug",
            "description": "Slug of the parent factor, when decomposed."
          },
          "parent_relation": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Parent Relation",
            "description": "How this factor relates to its parent."
          },
          "child_slugs": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Child Slugs",
            "description": "Slugs of component factors — the decomposition."
          },
          "is_headline": {
            "type": "boolean",
            "title": "Is Headline",
            "description": "True for a headline (total) factor."
          },
          "is_derived": {
            "type": "boolean",
            "title": "Is Derived",
            "description": "True for a factor derived from others."
          },
          "value_co2e": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Value Co2E",
            "description": "Default kgCO2e value at the latest reference year; null when value_withheld is true."
          },
          "co2e_basis": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Co2E Basis",
            "description": "What the CO2e number aggregates."
          },
          "ghg_breakdown": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/JsonValue"
              },
              {
                "type": "null"
              }
            ],
            "description": "Per-gas breakdown, publisher-shaped JSON."
          },
          "gwp_method": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Gwp Method",
            "description": "GWP methodology of the default value."
          },
          "gwp_horizon_years": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Gwp Horizon Years",
            "description": "GWP horizon."
          },
          "calculation_approach": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Calculation Approach",
            "description": "How the publisher computed the value."
          },
          "reference_year": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reference Year",
            "description": "Reference year of the default value."
          },
          "confidence_tier": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Confidence Tier",
            "description": "Cross-library confidence ordinal."
          },
          "data_quality_rating": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Data Quality Rating",
            "description": "The publisher's own quality rating."
          },
          "data_quality_scheme": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Data Quality Scheme",
            "description": "The scheme that rating is expressed in."
          },
          "license_tier": {
            "type": "string",
            "enum": [
              "open",
              "copyleft",
              "restricted",
              "prohibited"
            ],
            "title": "License Tier",
            "description": "Licence tier of the library."
          },
          "license_code": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "License Code",
            "description": "Licence identifier."
          },
          "attribution_text": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Attribution Text",
            "description": "The attribution line reuse requires."
          },
          "value_withheld": {
            "type": "boolean",
            "title": "Value Withheld",
            "description": "True when the licence gate serves this record as metadata only: every value field is withheld server-side."
          },
          "depth": {
            "type": "string",
            "enum": [
              "headline",
              "full"
            ],
            "title": "Depth",
            "description": "The depth this response was rendered at."
          },
          "matrix": {
            "$ref": "#/components/schemas/FactorMatrix",
            "description": "The year-by-methodology matrix: shape always, values at full."
          }
        },
        "type": "object",
        "required": [
          "slug",
          "library",
          "is_headline",
          "is_derived",
          "license_tier",
          "value_withheld",
          "depth",
          "matrix"
        ],
        "title": "FactorDetail",
        "description": "The full public record of one emission factor."
      },
      "FactorListPage": {
        "properties": {
          "kind": {
            "type": "string",
            "const": "factors",
            "title": "Kind",
            "description": "Discriminator: a results page.",
            "default": "factors"
          },
          "rows": {
            "items": {
              "$ref": "#/components/schemas/FactorListRow"
            },
            "type": "array",
            "title": "Rows",
            "description": "The page of factors."
          },
          "total": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Total",
            "description": "Factors matching the whole selection. Null on a searched (q) response, which is a ranked top-N without counts."
          },
          "facets": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/FacetBlock"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Facets",
            "description": "Facet counts for the current selection. Null on a searched (q) response; always present on a filter-only selection."
          },
          "columns": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Columns",
            "description": "Column-registry ids populated on each row, in display order."
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor",
            "description": "Cursor for the next page; null on the last page."
          }
        },
        "type": "object",
        "required": [
          "rows",
          "columns"
        ],
        "title": "FactorListPage",
        "description": "A page of the factor catalogue, with the facet block for the selection."
      },
      "FactorListRow": {
        "properties": {
          "slug": {
            "type": "string",
            "title": "Slug",
            "description": "Stable public identifier; the detail URL key."
          },
          "library": {
            "type": "string",
            "title": "Library",
            "description": "Library (publisher) code."
          },
          "license_tier": {
            "type": "string",
            "enum": [
              "open",
              "copyleft",
              "restricted",
              "prohibited"
            ],
            "title": "License Tier",
            "description": "Licence tier of the factor's library."
          },
          "value_withheld": {
            "type": "boolean",
            "title": "Value Withheld",
            "description": "True when the licence gate serves this row as metadata only: every value field is withheld server-side, never null-by-accident."
          },
          "activity_full": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Activity Full",
            "description": "Full activity label, in the resolved language."
          },
          "sector": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sector",
            "description": "Top activity level."
          },
          "sub_sector": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sub Sector",
            "description": "Leaf activity level."
          },
          "region_code": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Region Code",
            "description": "Geography code."
          },
          "unit_code": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Unit Code",
            "description": "Denominator unit."
          },
          "unit_family": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Unit Family",
            "description": "Unit family, e.g. mass, energy, monetary."
          },
          "system_boundary": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "System Boundary",
            "description": "The factor's system boundary name."
          },
          "boundary_family": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Boundary Family",
            "description": "Cross-library boundary family."
          },
          "reference_year": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reference Year",
            "description": "Reference year of the default value shown."
          },
          "value_co2e": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Value Co2E",
            "description": "Default kgCO2e value at the latest reference year. Null when value_withheld is true or the column is not selected."
          },
          "gwp_method": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Gwp Method",
            "description": "GWP methodology of the value shown."
          },
          "confidence_tier": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Confidence Tier",
            "description": "Cross-library confidence ordinal."
          },
          "is_derived": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Is Derived",
            "description": "True for a factor derived from others."
          }
        },
        "type": "object",
        "required": [
          "slug",
          "library",
          "license_tier",
          "value_withheld"
        ],
        "title": "FactorListRow",
        "description": "One factor in the list projection.\n\nIdentity fields (slug, library, license_tier, value_withheld) are always\npresent. Every other field is populated only when its column-registry id\nis in the effective column selection, and null otherwise."
      },
      "FactorMatrix": {
        "properties": {
          "shape": {
            "$ref": "#/components/schemas/FactorMatrixShape",
            "description": "Counts, year range and methodology names — at every depth."
          },
          "values": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/FactorMatrixCell"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Values",
            "description": "The cells. Null at headline depth, present at full."
          }
        },
        "type": "object",
        "required": [
          "shape"
        ],
        "title": "FactorMatrix",
        "description": "The year-by-methodology matrix: shape always, values by depth."
      },
      "FactorMatrixCell": {
        "properties": {
          "reference_year": {
            "type": "integer",
            "title": "Reference Year",
            "description": "Reference year of this value."
          },
          "gwp_method": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Gwp Method",
            "description": "GWP methodology of this value."
          },
          "gwp_horizon_years": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Gwp Horizon Years",
            "description": "GWP horizon, e.g. 100."
          },
          "value_co2e": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Value Co2E",
            "description": "kgCO2e value; null when value_withheld is true."
          },
          "value_withheld": {
            "type": "boolean",
            "title": "Value Withheld",
            "description": "True when the licence gate strips this value server-side."
          },
          "is_default": {
            "type": "boolean",
            "title": "Is Default",
            "description": "True for the combination the headline value is drawn from."
          },
          "is_latest_year": {
            "type": "boolean",
            "title": "Is Latest Year",
            "description": "True for the factor's latest reference year."
          }
        },
        "type": "object",
        "required": [
          "reference_year",
          "value_withheld",
          "is_default",
          "is_latest_year"
        ],
        "title": "FactorMatrixCell",
        "description": "One year-and-methodology combination, served at full depth only."
      },
      "FactorMatrixShape": {
        "properties": {
          "combination_count": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Combination Count",
            "description": "Distinct year-and-methodology combinations this factor carries."
          },
          "year_min": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Year Min",
            "description": "Earliest reference year; null when none."
          },
          "year_max": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Year Max",
            "description": "Latest reference year; null when none."
          },
          "methodologies": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Methodologies",
            "description": "Methodology names present, e.g. GWP method identifiers."
          }
        },
        "type": "object",
        "required": [
          "combination_count",
          "methodologies"
        ],
        "title": "FactorMatrixShape",
        "description": "The shape of the year-by-methodology matrix — always served."
      },
      "FileContextInput": {
        "properties": {
          "currency": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Currency",
            "description": "ISO 4217 currency code (e.g. ``DKK``). Forwarded verbatim from ``plan_emission_factor_mapping``'s suggested.file_context.currency.value; omit when the heuristic detected a per-row currency column or when the LLM returned ``null``."
          },
          "country": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Country",
            "description": "ISO 3166-1 alpha-2 buyer country (e.g. ``DK``). Forwarded verbatim from ``plan_emission_factor_mapping``'s suggested.file_context.country.value."
          },
          "monetary_scale": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Monetary Scale",
            "description": "File-level monetary magnitude: 'thousands' / 'millions'. Forwarded verbatim from ``plan_emission_factor_mapping``'s ``suggested.file_context.monetary_scale``; the worker threads it into the classifier so amounts stated in k€ / 'EUR ''000' at the column/header level scale correctly. Omit when amounts are at face value."
          },
          "input_description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Input Description",
            "description": "2-3 sentence file-level summary of the company's sector and the file's content. Forwarded verbatim from ``plan_emission_factor_mapping``'s ``suggested.file_context.input_description``; the worker threads it into the reformulator and rerankers (system-prompt prefix for the LLM stages, query prefix for the external API reranker) so they can disambiguate unknown brand names and short codes."
          },
          "measure_pairs": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/MeasurePair"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Measure Pairs",
            "description": "Nominated measure column pairs (docs/plans/pipeline/FEA-0003-2026-07-03-measure-role-binding.md §4.1): which columns carry a measure's number and its unit / currency token — pairing only, never a basis (one unit column may mix MWh, EUR and pcs across rows; the basis is resolved per row deterministically). In server-inference mode forward ``suggested.file_context.measure_pairs`` verbatim; in delegated mode nominate them yourself from the column profile (see the plan tool's Delegated file-context instructions). Semantics: OMITTED (null) = not nominated — the server re-runs its own nomination as a fallback; an EXPLICIT empty list = deliberate abstention (no pair form matches this table) — binding stays off, no fallback. Never invent a pair: a wrong pair is worse than none."
          },
          "domain": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/DomainValue"
              },
              {
                "type": "null"
              }
            ],
            "description": "File-level domain classification (``plastics_compounding`` …) from ``create_lexicon``. Forward verbatim when a lexicon was built; the library-routing classifier uses it to shortcut routing. Spec 07-file-context-glossary.md §4/§8."
          },
          "lexicon": {
            "items": {
              "$ref": "#/components/schemas/LexiconEntry"
            },
            "type": "array",
            "title": "Lexicon",
            "description": "Decoded file vocabulary from ``create_lexicon`` (brand / abbreviation / technical_term entries). Forward the entries verbatim; the worker injects the row-relevant ones into the per-row reformulator so cheaper models see opaque tokens pre-decoded. Empty when no lexicon was built. Spec §4/§8."
          },
          "lexicon_audit": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LexiconAudit"
              },
              {
                "type": "null"
              }
            ],
            "description": "Provenance + telemetry for the lexicon build, persisted for audit on ``mcp.jobs.params``. ``None`` when no lexicon was built."
          },
          "confidence": {
            "anyOf": [
              {
                "additionalProperties": {
                  "type": "number"
                },
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Confidence",
            "description": "Per-field confidence (``{'currency': 0.94, 'country': 0.91}``). Stored for audit only — the pipeline does not condition on it."
          },
          "evidence": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Evidence",
            "description": "Free-text justification from the inspector LLM, persisted for audit on ``mcp.jobs.params``."
          },
          "model": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Model",
            "description": "LLM model that produced the inference (audit only)."
          },
          "model_version": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Model Version",
            "description": "Prompt version constant from the inspector at inference time."
          },
          "source": {
            "type": "string",
            "enum": [
              "file_inspector",
              "user",
              "client"
            ],
            "title": "Source",
            "description": "Where this context block came from. ``file_inspector`` for values inferred by ``plan_emission_factor_mapping``; ``user`` when the LLM client got an explicit override from the user; ``client`` when the calling agent supplied a default.",
            "default": "file_inspector"
          }
        },
        "type": "object",
        "title": "FileContextInput",
        "description": "File-level context overrides forwarded into the pipeline.\n\nPersisted verbatim on ``mcp.jobs.params.file_context`` so the job's\ninferred assumptions are auditable (spec §5.1). The worker reads\n``currency`` and ``country`` and threads them into\n:class:`SearchPipelineConfig` so the line-item classifier and LLM\nreranker prompts gain \"amounts are in <currency>\" / \"buyer is in\n<country>\" conditioning."
      },
      "InspectFileColumnProfile": {
        "properties": {
          "name": {
            "type": "string",
            "title": "Name"
          },
          "dtype": {
            "type": "string",
            "enum": [
              "string",
              "number",
              "mixed",
              "empty"
            ],
            "title": "Dtype"
          },
          "pct_non_null": {
            "type": "number",
            "maximum": 1.0,
            "minimum": 0.0,
            "title": "Pct Non Null"
          },
          "samples": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Samples",
            "description": "Up to 3 stringified non-null sample values."
          }
        },
        "type": "object",
        "required": [
          "name",
          "dtype",
          "pct_non_null"
        ],
        "title": "InspectFileColumnProfile",
        "description": "Per-column shape summary computed from the best header candidate."
      },
      "InspectFileContextBlock": {
        "properties": {
          "currency": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/InspectFileContextValue"
              },
              {
                "type": "null"
              }
            ],
            "description": "ISO 4217 currency. Omitted (``None``) when a per-row currency column was detected — the per-row value is authoritative."
          },
          "country": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/InspectFileContextValue"
              },
              {
                "type": "null"
              }
            ],
            "description": "ISO 3166-1 alpha-2 buyer country."
          },
          "monetary_scale": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Monetary Scale",
            "description": "File-level monetary magnitude: 'thousands' / 'millions' / None. Forward verbatim into ``map_procurement_items_to_emission_factors``'s ``file_context`` so amounts stated in k€ / 'EUR ''000' at the column or header level scale correctly. ``None`` when amounts are at face value."
          },
          "input_description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Input Description",
            "description": "2-3 sentence file-level summary of the company's sector and the file's content. Forward verbatim into ``map_procurement_items_to_emission_factors``'s ``file_context``; it is threaded into the reformulator and rerankers to disambiguate unknown brand names and short codes."
          },
          "measure_pairs": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/MeasurePair"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Measure Pairs",
            "description": "Nominated measure column pairs (docs/plans/pipeline/FEA-0003-2026-07-03-measure-role-binding.md §4.1): which columns carry a measure's number and its unit / currency token. Forward verbatim into ``map_…``'s ``file_context.measure_pairs`` — they drive the deterministic per-row measure builder. An empty list is a deliberate abstention (no pair form matches this table); ``None`` means not nominated."
          },
          "evidence": {
            "type": "string",
            "title": "Evidence",
            "description": "Short free-text justification quoting the signals used."
          },
          "model": {
            "type": "string",
            "title": "Model",
            "description": "Model name that produced this inference."
          },
          "model_version": {
            "type": "string",
            "title": "Model Version",
            "description": "Prompt version constant — bumping it busts the LLM cache. Spec §6.4."
          }
        },
        "type": "object",
        "required": [
          "evidence",
          "model",
          "model_version"
        ],
        "title": "InspectFileContextBlock",
        "description": "File-level LLM-inferred context block, surfaced inside ``suggested``."
      },
      "InspectFileContextValue": {
        "properties": {
          "value": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Value",
            "description": "Inferred value (ISO 4217 for currency, ISO 3166-1 alpha-2 for country). ``None`` is a definite answer when the file has no monetary signal at all; otherwise read together with ``confidence``."
          },
          "confidence": {
            "type": "number",
            "maximum": 1.0,
            "minimum": 0.0,
            "title": "Confidence"
          }
        },
        "type": "object",
        "required": [
          "confidence"
        ],
        "title": "InspectFileContextValue",
        "description": "One inferred file-level value and its self-reported confidence."
      },
      "InspectFileHeaderCandidate": {
        "properties": {
          "row_index": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Row Index",
            "description": "0-based row index inside the sheet's sample window."
          },
          "confidence": {
            "type": "number",
            "maximum": 1.0,
            "minimum": 0.0,
            "title": "Confidence"
          },
          "reason": {
            "type": "string",
            "title": "Reason",
            "description": "Human-readable rationale."
          }
        },
        "type": "object",
        "required": [
          "row_index",
          "confidence",
          "reason"
        ],
        "title": "InspectFileHeaderCandidate",
        "description": "One row scored as a possible header.\n\nSorted descending by ``confidence`` inside\n:class:`InspectFileSheetProfile.header_candidates`. The LLM picks the top\ncandidate when ``confidence`` clears the threshold and otherwise asks the\nuser (driven by ``InspectFileOutput.needs_clarification``)."
      },
      "InspectFileSheetProfile": {
        "properties": {
          "name": {
            "type": "string",
            "title": "Name"
          },
          "n_rows": {
            "type": "integer",
            "minimum": 0.0,
            "title": "N Rows",
            "description": "Rows seen inside the sample window (capped server-side)."
          },
          "total_rows": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Total Rows",
            "description": "Non-blank rows across the WHOLE sheet (header included). Report THIS as the file's row count, not n_rows — n_rows is only the profiled sample window.",
            "default": 0
          },
          "sampled": {
            "type": "boolean",
            "title": "Sampled",
            "description": "True when total_rows > n_rows, i.e. the profile reflects only the first rows of a larger sheet.",
            "default": false
          },
          "n_cols": {
            "type": "integer",
            "minimum": 0.0,
            "title": "N Cols"
          },
          "looks_like_data": {
            "type": "boolean",
            "title": "Looks Like Data"
          },
          "looks_like_data_reason": {
            "type": "string",
            "title": "Looks Like Data Reason"
          },
          "header_candidates": {
            "items": {
              "$ref": "#/components/schemas/InspectFileHeaderCandidate"
            },
            "type": "array",
            "title": "Header Candidates"
          },
          "preview_rows": {
            "items": {
              "items": {
                "anyOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "type": "array"
            },
            "type": "array",
            "title": "Preview Rows",
            "description": "First N raw rows as nullable strings, no header inference."
          },
          "column_profile": {
            "items": {
              "$ref": "#/components/schemas/InspectFileColumnProfile"
            },
            "type": "array",
            "title": "Column Profile"
          }
        },
        "type": "object",
        "required": [
          "name",
          "n_rows",
          "n_cols",
          "looks_like_data",
          "looks_like_data_reason"
        ],
        "title": "InspectFileSheetProfile",
        "description": "Shape of a single sheet (or the CSV's sole sheet)."
      },
      "InspectFileSuggestedShape": {
        "properties": {
          "sheet_name": {
            "type": "string",
            "title": "Sheet Name"
          },
          "header_row": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Header Row",
            "description": "0-based index of the header row inside the sheet."
          },
          "skip_trailing_rows": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Skip Trailing Rows",
            "default": 0
          },
          "useful_columns": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Useful Columns"
          },
          "file_context": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/InspectFileContextBlock"
              },
              {
                "type": "null"
              }
            ],
            "description": "File-level inferred context (currency + country) and provenance. Spec: docs/feature-candidates/new-mcp-tools/05-done-file-context-inference.md §4.2. Present only when the LLM call succeeded; ``None`` when inference was skipped (caller opted out, no LLM provider configured) or the call failed."
          }
        },
        "type": "object",
        "required": [
          "sheet_name",
          "header_row"
        ],
        "title": "InspectFileSuggestedShape",
        "description": "Best-guess interpretation a downstream caller can forward verbatim.\n\nMirrors the override fields on ``map_procurement_items_to_emission_factors``\nso the LLM can pass each field straight through."
      },
      "InspectRequest": {
        "properties": {
          "skip_file_context": {
            "type": "boolean",
            "title": "Skip File Context",
            "description": "Skip the file-level inference (currency, country, monetary scale, measure pairs) when the user already stated them. Saves roughly 500 ms and leaves ``suggested.file_context`` null.",
            "default": false
          }
        },
        "type": "object",
        "title": "InspectRequest",
        "description": "Body of ``POST /v1/uploads/{upload_id}/inspect`` (spec §4.3)."
      },
      "InspectResponse": {
        "properties": {
          "upload_id": {
            "type": "string",
            "format": "uuid",
            "title": "Upload Id"
          },
          "filename": {
            "type": "string",
            "title": "Filename"
          },
          "format": {
            "type": "string",
            "enum": [
              "xlsx",
              "xls",
              "csv"
            ],
            "title": "Format"
          },
          "size_bytes": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Size Bytes"
          },
          "confidence": {
            "type": "number",
            "maximum": 1.0,
            "minimum": 0.0,
            "title": "Confidence",
            "description": "Header-pick confidence on the suggested sheet; 0 when no sheet looked like data."
          },
          "sheets": {
            "items": {
              "$ref": "#/components/schemas/InspectFileSheetProfile"
            },
            "type": "array",
            "title": "Sheets"
          },
          "suggested": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/InspectFileSuggestedShape"
              },
              {
                "type": "null"
              }
            ],
            "description": "``null`` when no sheet looked like tabular data — then ``clarification`` carries the question to put to the user."
          },
          "messages": {
            "items": {
              "$ref": "#/components/schemas/Message"
            },
            "type": "array",
            "title": "Messages"
          },
          "clarification": {
            "$ref": "#/components/schemas/Clarification"
          }
        },
        "type": "object",
        "required": [
          "upload_id",
          "filename",
          "format",
          "size_bytes",
          "confidence"
        ],
        "title": "InspectResponse",
        "description": "``200`` body of ``POST /v1/uploads/{upload_id}/inspect`` (spec §4.3).\n\nRow indices are 0-based on the wire and 1-based in ``messages[].text``.\n``suggested`` is designed to be forwarded verbatim into §4.5 once the user\nhas had a chance to correct it."
      },
      "JobListEntry": {
        "properties": {
          "job_id": {
            "type": "string",
            "format": "uuid",
            "title": "Job Id"
          },
          "client_reference": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Client Reference"
          },
          "filename": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Filename"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "succeeded",
              "failed"
            ],
            "title": "Status"
          },
          "row_count": {
            "type": "integer",
            "title": "Row Count",
            "default": 0
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "title": "Submitted At"
          },
          "finished_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Finished At"
          },
          "total_emissions_kgco2e": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Total Emissions Kgco2E"
          }
        },
        "type": "object",
        "required": [
          "job_id",
          "status",
          "submitted_at"
        ],
        "title": "JobListEntry",
        "description": "One row of ``GET /v1/mapping-jobs`` (spec §4.12)."
      },
      "JobListResponse": {
        "properties": {
          "jobs": {
            "items": {
              "$ref": "#/components/schemas/JobListEntry"
            },
            "type": "array",
            "title": "Jobs"
          },
          "messages": {
            "items": {
              "$ref": "#/components/schemas/Message"
            },
            "type": "array",
            "title": "Messages"
          }
        },
        "type": "object",
        "title": "JobListResponse",
        "description": "``200`` body of ``GET /v1/mapping-jobs`` (spec §4.12)."
      },
      "JobResultResponse": {
        "properties": {
          "job_id": {
            "type": "string",
            "format": "uuid",
            "title": "Job Id"
          },
          "client_reference": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Client Reference"
          },
          "status": {
            "type": "string",
            "const": "succeeded",
            "title": "Status",
            "default": "succeeded"
          },
          "row_count": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Row Count"
          },
          "elapsed_seconds": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Elapsed Seconds"
          },
          "finished_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Finished At"
          },
          "downloads": {
            "$ref": "#/components/schemas/ResultDownloads"
          },
          "report_stats": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/EmissionReportStats"
              },
              {
                "type": "null"
              }
            ],
            "description": "``null`` only when the result file could not be read back for statistics; the rows themselves are still available."
          },
          "messages": {
            "items": {
              "$ref": "#/components/schemas/Message"
            },
            "type": "array",
            "title": "Messages"
          }
        },
        "type": "object",
        "required": [
          "job_id",
          "row_count",
          "downloads"
        ],
        "title": "JobResultResponse",
        "description": "``200`` body of ``GET /v1/mapping-jobs/{job_id}/result`` (spec §4.8).\n\nAlso embedded under ``result`` in the SSE ``succeeded`` event."
      },
      "JobStatusResponse": {
        "properties": {
          "job_id": {
            "type": "string",
            "format": "uuid",
            "title": "Job Id"
          },
          "client_reference": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Client Reference"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "succeeded",
              "failed"
            ],
            "title": "Status"
          },
          "stage": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "parsing",
                  "mapping",
                  "auditing"
                ]
              },
              {
                "type": "null"
              }
            ],
            "title": "Stage",
            "description": "``parsing`` → ``mapping`` → ``auditing``. ``null`` while queued."
          },
          "processed_rows": {
            "type": "integer",
            "title": "Processed Rows",
            "default": 0
          },
          "total_rows": {
            "type": "integer",
            "title": "Total Rows",
            "default": 0
          },
          "percent": {
            "type": "number",
            "title": "Percent",
            "description": "Progress within the current stage. It resets at the parsing→mapping boundary, so it can move backwards.",
            "default": 0.0
          },
          "queue_position": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Queue Position"
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "title": "Submitted At"
          },
          "started_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Started At"
          },
          "finished_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Finished At"
          },
          "last_progress_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Progress At"
          },
          "elapsed_running_seconds": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Elapsed Running Seconds"
          },
          "seconds_since_last_progress": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Seconds Since Last Progress"
          },
          "retry_after_seconds": {
            "type": "number",
            "title": "Retry After Seconds",
            "default": 15.0
          },
          "error_message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error Message",
            "description": "One end-user sentence on a failed job, ``null`` otherwise. Never the raw exception — see :mod:`open_climate_ai.api.rest.failure`."
          },
          "messages": {
            "items": {
              "$ref": "#/components/schemas/Message"
            },
            "type": "array",
            "title": "Messages"
          }
        },
        "type": "object",
        "required": [
          "job_id",
          "status",
          "submitted_at"
        ],
        "title": "JobStatusResponse",
        "description": "``200`` body of ``GET /v1/mapping-jobs/{job_id}`` (spec §4.7).\n\nAlso the payload of the SSE ``snapshot`` event. Honour\n``retry_after_seconds`` — 15 while running, 0 once terminal."
      },
      "JsonValue": {},
      "LexiconAudit": {
        "properties": {
          "executor": {
            "type": "string",
            "enum": [
              "server_agent",
              "delegated_client"
            ],
            "title": "Executor",
            "description": "Who ran the build — the server ReAct agent or the MCP client."
          },
          "model": {
            "type": "string",
            "title": "Model",
            "description": "Frontier model id that produced the lexicon."
          },
          "model_version": {
            "type": "string",
            "title": "Model Version",
            "description": "``LEXICON_PROMPT_VERSION`` constant at build time (cache key)."
          },
          "n_turns": {
            "type": "integer",
            "minimum": 0.0,
            "title": "N Turns",
            "default": 0
          },
          "n_code_runs": {
            "type": "integer",
            "minimum": 0.0,
            "title": "N Code Runs",
            "default": 0
          },
          "n_rows_scanned": {
            "type": "integer",
            "minimum": 0.0,
            "title": "N Rows Scanned",
            "default": 0
          },
          "web_search_used": {
            "type": "boolean",
            "title": "Web Search Used",
            "default": false
          },
          "search_count": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Search Count",
            "default": 0
          },
          "cost_eur": {
            "type": "number",
            "minimum": 0.0,
            "title": "Cost Eur",
            "default": 0.0
          },
          "elapsed_ms": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Elapsed Ms",
            "default": 0
          },
          "cap_hit": {
            "type": "boolean",
            "title": "Cap Hit",
            "description": "A hard cap (max_searches / max_cost_eur / max_turns) was hit.",
            "default": false
          },
          "failed": {
            "type": "boolean",
            "title": "Failed",
            "description": "The build failed; ``lexicon`` is empty and downstream runs without it.",
            "default": false
          }
        },
        "type": "object",
        "required": [
          "executor",
          "model",
          "model_version"
        ],
        "title": "LexiconAudit",
        "description": "Provenance + agent-loop telemetry for a lexicon build (spec §4)."
      },
      "LexiconEntry": {
        "properties": {
          "token": {
            "type": "string",
            "title": "Token",
            "description": "The recurring file token as it appears in rows (e.g. ``ISONYL``)."
          },
          "kind": {
            "type": "string",
            "enum": [
              "brand",
              "abbreviation",
              "technical_term"
            ],
            "title": "Kind",
            "description": "Token kind — brand / abbreviation / technical_term."
          },
          "interpretation": {
            "type": "string",
            "title": "Interpretation",
            "description": "Plain-language decode the cheap per-row model gets for free (e.g. 'polyamide 6 compound, often glass-filled')."
          },
          "evidence": {
            "type": "string",
            "title": "Evidence",
            "description": "Why this interpretation — separating file-internal evidence (co-occurrence across rows) from external evidence (web)."
          },
          "evidence_urls": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Evidence Urls",
            "description": "Source URLs when the decode came from web search; empty otherwise."
          },
          "n_rows": {
            "type": "integer",
            "minimum": 0.0,
            "title": "N Rows",
            "description": "Real occurrence count across the WHOLE file (from the full-row scan, not a sample proxy).",
            "default": 0
          },
          "confidence": {
            "type": "number",
            "maximum": 1.0,
            "minimum": 0.0,
            "title": "Confidence",
            "description": "Self-reported 0..1 confidence; a ranking signal, not calibrated (spec §11.3). Entries below the row-match floor are dropped."
          },
          "match_pattern": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Match Pattern",
            "description": "For ``abbreviation`` tokens with placeholder slots (``GFxx``), a Python regex compiled at build time that the per-row matcher applies instead of a literal substring (``GF\\d{1,2}``). ``None`` for literal tokens, which match by normalized word-boundary substring. The matching is deliberately exact, never fuzzy (spec §8.2)."
          }
        },
        "type": "object",
        "required": [
          "token",
          "kind",
          "interpretation",
          "evidence",
          "confidence"
        ],
        "title": "LexiconEntry",
        "description": "One decoded file vocabulary token (spec §4)."
      },
      "LibraryCatalogueResponse": {
        "properties": {
          "libraries": {
            "items": {
              "$ref": "#/components/schemas/LibraryEntry"
            },
            "type": "array",
            "title": "Libraries"
          },
          "messages": {
            "items": {
              "$ref": "#/components/schemas/Message"
            },
            "type": "array",
            "title": "Messages"
          }
        },
        "type": "object",
        "title": "LibraryCatalogueResponse",
        "description": "``200`` body of ``GET /v1/libraries`` (spec §4.4)."
      },
      "LibraryEntry": {
        "properties": {
          "code": {
            "type": "string",
            "title": "Code",
            "description": "The wire code. Send these verbatim in ``libraries``."
          },
          "name": {
            "type": "string",
            "title": "Name",
            "description": "Human label for the picker."
          },
          "description": {
            "type": "string",
            "title": "Description",
            "description": "One or two sentences on what the library covers, for a tooltip beside the checkbox."
          },
          "factor_count": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Factor Count",
            "description": "Factors a search can actually reach: embedded rows at the library's current edition. Superseded editions are excluded, so this is smaller than the publisher's lifetime total."
          },
          "basis": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Basis",
            "description": "The quantification bases this library can answer on — ``activity_based_physical_unit``, ``activity_based_item_count``, ``monetary``. A library is only reached for a line quantified on one of them."
          },
          "routes": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Routes",
            "description": "Internal route names this library serves. Diagnostic: it is what ``library_filtered_out`` rows are attributed to."
          },
          "default_selected": {
            "type": "boolean",
            "title": "Default Selected",
            "description": "Whether a picker should start with this library ticked. Every catalogued library is on by default — omitting ``libraries`` searches all of them.",
            "default": true
          }
        },
        "type": "object",
        "required": [
          "code",
          "name",
          "description",
          "factor_count"
        ],
        "title": "LibraryEntry",
        "description": "One selectable emission-factor library (spec §4.4)."
      },
      "LibraryWorkbook": {
        "properties": {
          "label": {
            "type": "string",
            "title": "Label",
            "description": "Human name of the workbook, e.g. the edition."
          },
          "format": {
            "type": "string",
            "title": "Format",
            "description": "File format, e.g. xlsx or csv."
          },
          "edition": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Edition",
            "description": "The edition label the workbook freezes."
          },
          "revision": {
            "type": "integer",
            "minimum": 1.0,
            "title": "Revision",
            "description": "2+ marks a corrected re-publication (-r2 filenames).",
            "default": 1
          },
          "rows": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Rows",
            "description": "Served factors frozen into the workbook."
          },
          "built_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Built At",
            "description": "When the workbook was built (UTC)."
          },
          "sha256": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sha256",
            "description": "Checksum of the artifact, from its manifest."
          },
          "filename": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Filename",
            "description": "The artifact's filename — what the authenticated download route takes to issue the URL."
          },
          "requires_account": {
            "type": "boolean",
            "title": "Requires Account",
            "description": "True when downloading needs a signed-in free account.",
            "default": true
          },
          "url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Url",
            "description": "Download URL. Always null on the anonymous listing — an authenticated principal obtains it from the /v1 exports surface."
          }
        },
        "type": "object",
        "required": [
          "label",
          "format"
        ],
        "title": "LibraryWorkbook",
        "description": "One downloadable edition workbook artifact of a library (FRN-0004)."
      },
      "MeasurePair": {
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "quantity_unit_column",
              "quantity_fixed_unit",
              "amount_currency_column",
              "amount_file_currency",
              "bare_quantity"
            ],
            "title": "Kind"
          },
          "quantity_column": {
            "type": "string",
            "title": "Quantity Column"
          },
          "unit_column": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Unit Column"
          },
          "fixed_unit": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Fixed Unit"
          },
          "assumed_basis": {
            "anyOf": [
              {
                "type": "string",
                "const": "items"
              },
              {
                "type": "null"
              }
            ],
            "title": "Assumed Basis"
          },
          "granularity": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "per_unit",
                  "total"
                ]
              },
              {
                "type": "null"
              }
            ],
            "title": "Granularity"
          },
          "evidence": {
            "type": "string",
            "title": "Evidence",
            "default": ""
          }
        },
        "type": "object",
        "required": [
          "kind",
          "quantity_column"
        ],
        "title": "MeasurePair",
        "description": "One nominated column pair (§4.1). Declares pairing, never basis.\n\nAttributes:\n    kind: Which of the five pair forms this is.\n    quantity_column: The number column (a quantity or a monetary amount).\n    unit_column: The per-row unit-token column (``quantity_unit_column``)\n        or per-row currency-code column (``amount_currency_column``).\n    fixed_unit: The unit named by the header for ``quantity_fixed_unit``\n        (``total_weight_kg`` → ``\"kg\"``).\n    assumed_basis: File-level basis assumption for ``bare_quantity``\n        (a number column with no unit anywhere) — recorded as\n        ``tier: assumption`` and yielded to any explicit row-level unit.\n    granularity: For a physical / monetary number column, whether it holds a\n        ``per_unit`` value (unit mass, unit price) or the line ``total``\n        (extended mass, line amount). ``None`` when the nominator gave no\n        signal — the deterministic header-cue pass fills it in\n        :func:`validate_measure_pairs`, and the builder defaults an unmarked\n        physical / monetary column to ``total`` (the line-ledger convention).\n    evidence: Free-text evidence quoted by the nominator (audit trail)."
      },
      "Message": {
        "properties": {
          "level": {
            "type": "string",
            "enum": [
              "info",
              "warning",
              "action_required"
            ],
            "title": "Level",
            "description": "``info``, ``warning`` or ``action_required``. Only ``action_required`` should block the user."
          },
          "code": {
            "type": "string",
            "enum": [
              "sheet_selected",
              "header_row_selected",
              "trailing_rows_skipped",
              "per_row_currency_detected",
              "currency_low_confidence",
              "country_low_confidence",
              "measure_pairs_note",
              "file_context_unavailable",
              "no_data_sheet",
              "job_queued",
              "library_filter_narrows_route",
              "progress",
              "job_succeeded",
              "rows_unmatched",
              "rows_missing_quantity",
              "rows_need_review",
              "rows_excluded_used_goods",
              "no_data_rows",
              "job_failed"
            ],
            "title": "Code",
            "description": "Stable machine identifier, for i18n and for suppressing messages the front end handles in its own UI."
          },
          "text": {
            "type": "string",
            "title": "Text",
            "description": "English end-user prose. Contains no instructions addressed to a model."
          },
          "fields": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Fields",
            "description": "Dotted request-field paths the message concerns, so the UI can highlight the right control."
          }
        },
        "type": "object",
        "required": [
          "level",
          "code",
          "text"
        ],
        "title": "Message",
        "description": "One end-user message. ``text`` is safe to render verbatim."
      },
      "PublicLibrary": {
        "properties": {
          "code": {
            "type": "string",
            "title": "Code",
            "description": "Library code — the list route's library filter value."
          },
          "name": {
            "type": "string",
            "title": "Name",
            "description": "Display name of the library."
          },
          "publisher": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Publisher",
            "description": "The organisation behind the library."
          },
          "edition": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Edition",
            "description": "The edition or version currently served."
          },
          "license_tier": {
            "type": "string",
            "enum": [
              "open",
              "copyleft",
              "restricted",
              "prohibited"
            ],
            "title": "License Tier",
            "description": "Licence tier: open and copyleft serve values, restricted serves metadata with values withheld, prohibited is not listed at all."
          },
          "license_code": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "License Code",
            "description": "Licence identifier, e.g. an SPDX-like code."
          },
          "attribution_text": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Attribution Text",
            "description": "The attribution line reuse requires."
          },
          "factors_served": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Factors Served",
            "description": "Factors of this library on the anonymous surface."
          },
          "values_served": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Values Served",
            "description": "Factors whose values are actually shown — fewer than factors_served on a restricted library, where values are withheld."
          },
          "year_min": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Year Min",
            "description": "Earliest reference year served; null when none."
          },
          "year_max": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Year Max",
            "description": "Latest reference year served; null when none."
          },
          "year_semantics": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Year Semantics",
            "description": "What this library's reference_year means — publication year, data year or validity start — so cross-library year filters are read with the right caveat."
          },
          "workbooks": {
            "items": {
              "$ref": "#/components/schemas/LibraryWorkbook"
            },
            "type": "array",
            "title": "Workbooks",
            "description": "Published edition workbooks of this library — metadata for everyone, download URLs only via the authenticated surface."
          }
        },
        "type": "object",
        "required": [
          "code",
          "name",
          "license_tier",
          "factors_served",
          "values_served"
        ],
        "title": "PublicLibrary",
        "description": "One emission-factor library on the anonymous surface."
      },
      "PublicLibraryList": {
        "properties": {
          "libraries": {
            "items": {
              "$ref": "#/components/schemas/PublicLibrary"
            },
            "type": "array",
            "title": "Libraries",
            "description": "Every served library, in the serving layer's stable order."
          }
        },
        "type": "object",
        "required": [
          "libraries"
        ],
        "title": "PublicLibraryList",
        "description": "The full library catalogue of the anonymous surface."
      },
      "ResultDownloads": {
        "properties": {
          "rows_json": {
            "type": "string",
            "title": "Rows Json"
          },
          "rows_csv": {
            "type": "string",
            "title": "Rows Csv"
          },
          "workbook_xlsx": {
            "type": "string",
            "title": "Workbook Xlsx"
          }
        },
        "type": "object",
        "required": [
          "rows_json",
          "rows_csv",
          "workbook_xlsx"
        ],
        "title": "ResultDownloads",
        "description": "Absolute URLs for the three ways to take the result away (§4.8)."
      },
      "RowsResponse": {
        "properties": {
          "job_id": {
            "type": "string",
            "format": "uuid",
            "title": "Job Id"
          },
          "filename": {
            "type": "string",
            "title": "Filename",
            "description": "The result file the rows were read from."
          },
          "row_count": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Row Count",
            "description": "Rows in the file, before filtering."
          },
          "filtered_row_count": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Filtered Row Count",
            "description": "Rows the filters kept, across every page."
          },
          "page": {
            "type": "integer",
            "minimum": 1.0,
            "title": "Page"
          },
          "page_size": {
            "type": "integer",
            "minimum": 1.0,
            "title": "Page Size"
          },
          "has_more": {
            "type": "boolean",
            "title": "Has More",
            "default": false
          },
          "next_page_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Page Url",
            "description": "``null`` on the last page."
          },
          "columns": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Columns",
            "description": "Every key present on every row, in display order."
          },
          "rows": {
            "items": {
              "additionalProperties": {
                "$ref": "#/components/schemas/JsonValue"
              },
              "type": "object"
            },
            "type": "array",
            "title": "Rows"
          },
          "messages": {
            "items": {
              "$ref": "#/components/schemas/Message"
            },
            "type": "array",
            "title": "Messages"
          }
        },
        "type": "object",
        "required": [
          "job_id",
          "filename",
          "row_count",
          "filtered_row_count",
          "page",
          "page_size"
        ],
        "title": "RowsResponse",
        "description": "``200`` body of ``GET /v1/mapping-jobs/{job_id}/rows`` (spec §4.9)."
      },
      "SignInRequiredEnvelope": {
        "properties": {
          "kind": {
            "type": "string",
            "const": "sign_in_required",
            "title": "Kind",
            "description": "Discriminator: the meter wall.",
            "default": "sign_in_required"
          },
          "total_matches": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Total Matches",
            "description": "Factors this selection matches — shown, never served."
          },
          "search_limit": {
            "type": "integer",
            "title": "Search Limit",
            "description": "Anonymous searches allowed per session (ten)."
          },
          "searches_used": {
            "type": "integer",
            "title": "Searches Used",
            "description": "Distinct filter signatures this session has spent."
          },
          "message": {
            "type": "string",
            "title": "Message",
            "description": "End-user prose naming the free account that lifts the limit."
          }
        },
        "type": "object",
        "required": [
          "total_matches",
          "search_limit",
          "searches_used",
          "message"
        ],
        "title": "SignInRequiredEnvelope",
        "description": "The answer once an anonymous session has spent its search meter.\n\nServed with HTTP 200 and Cache-Control no-store: it is per-visitor state,\nnot content, and not an error — the same request signed in returns the\nresults page."
      },
      "UploadResponse": {
        "properties": {
          "upload_id": {
            "type": "string",
            "format": "uuid",
            "title": "Upload Id",
            "description": "Handle for every later step."
          },
          "status": {
            "type": "string",
            "const": "uploaded",
            "title": "Status",
            "default": "uploaded"
          },
          "filename": {
            "type": "string",
            "title": "Filename"
          },
          "file_path": {
            "type": "string",
            "title": "File Path",
            "description": "Server-side ``s3://`` URI. Display only — downstream routes take ``upload_id``."
          },
          "content_type": {
            "type": "string",
            "title": "Content Type"
          },
          "size_bytes": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Size Bytes"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "uploaded_at": {
            "type": "string",
            "format": "date-time",
            "title": "Uploaded At"
          },
          "max_bytes": {
            "type": "integer",
            "minimum": 0.0,
            "title": "Max Bytes",
            "description": "Largest body this deployment accepts, in bytes."
          },
          "messages": {
            "items": {
              "$ref": "#/components/schemas/Message"
            },
            "type": "array",
            "title": "Messages"
          }
        },
        "type": "object",
        "required": [
          "upload_id",
          "filename",
          "file_path",
          "content_type",
          "size_bytes",
          "created_at",
          "uploaded_at",
          "max_bytes"
        ],
        "title": "UploadResponse",
        "description": "``201`` body of ``POST /v1/uploads`` (spec §4.1)."
      },
      "UploadSlotRequest": {
        "properties": {
          "filename": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Filename",
            "description": "Display-only hint. The realised name comes from the multipart part the browser sends."
          }
        },
        "type": "object",
        "title": "UploadSlotRequest",
        "description": "Body of ``POST /v1/upload-slots`` (spec §4.2). Everything optional."
      },
      "UploadSlotResponse": {
        "properties": {
          "upload_id": {
            "type": "string",
            "format": "uuid",
            "title": "Upload Id"
          },
          "upload_url": {
            "type": "string",
            "title": "Upload Url",
            "description": "Capability URL carrying its own credential. Safe to hand to a browser; never send the partner bearer token with it."
          },
          "file_prefix": {
            "type": "string",
            "title": "File Prefix",
            "description": "Storage prefix the bytes will land under (display only)."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "title": "Expires At"
          },
          "messages": {
            "items": {
              "$ref": "#/components/schemas/Message"
            },
            "type": "array",
            "title": "Messages"
          }
        },
        "type": "object",
        "required": [
          "upload_id",
          "upload_url",
          "file_prefix",
          "expires_at"
        ],
        "title": "UploadSlotResponse",
        "description": "``201`` body of ``POST /v1/upload-slots`` (spec §4.2)."
      },
      "UploadStatusResponse": {
        "properties": {
          "upload_id": {
            "type": "string",
            "format": "uuid",
            "title": "Upload Id"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "uploaded",
              "expired"
            ],
            "title": "Status",
            "description": "``pending`` before the bytes arrive, ``uploaded`` after, ``expired`` once a capability URL lapsed with nothing uploaded."
          },
          "filename": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Filename"
          },
          "size_bytes": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Size Bytes"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "uploaded_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Uploaded At"
          },
          "messages": {
            "items": {
              "$ref": "#/components/schemas/Message"
            },
            "type": "array",
            "title": "Messages"
          }
        },
        "type": "object",
        "required": [
          "upload_id",
          "status",
          "created_at"
        ],
        "title": "UploadStatusResponse",
        "description": "``200`` body of ``GET /v1/uploads/{upload_id}`` (spec §4.2)."
      },
      "ErrorBody": {
        "description": "The ``error`` object carried by every failing ``/v1`` response.",
        "properties": {
          "code": {
            "$ref": "#/components/schemas/ErrorCode",
            "description": "Stable machine identifier for the failure."
          },
          "message": {
            "description": "End-user readable explanation.",
            "title": "Message",
            "type": "string"
          },
          "status": {
            "description": "HTTP status, repeated inside the body.",
            "title": "Status",
            "type": "integer"
          },
          "request_id": {
            "description": "Correlation handle; quote it in reports.",
            "title": "Request Id",
            "type": "string"
          }
        },
        "required": [
          "code",
          "message",
          "status",
          "request_id"
        ],
        "title": "ErrorBody",
        "type": "object"
      },
      "ErrorEnvelope": {
        "description": "Top-level error response body.",
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorBody"
          }
        },
        "required": [
          "error"
        ],
        "title": "ErrorEnvelope",
        "type": "object"
      },
      "ErrorCode": {
        "type": "string",
        "enum": [
          "invalid_request",
          "unauthenticated",
          "forbidden",
          "account_not_approved",
          "upload_not_found",
          "job_not_found",
          "user_not_found",
          "job_not_ready",
          "job_failed",
          "factor_not_found",
          "edition_not_found",
          "file_too_large",
          "unreadable_file",
          "rate_limited",
          "service_unavailable",
          "internal_error"
        ],
        "description": "Stable machine identifier for a failure. The list grows: treat an unrecognised code as a generic failure of its HTTP status rather than as an error in itself."
      },
      "MessageCode": {
        "type": "string",
        "enum": [
          "sheet_selected",
          "header_row_selected",
          "trailing_rows_skipped",
          "per_row_currency_detected",
          "currency_low_confidence",
          "country_low_confidence",
          "measure_pairs_note",
          "file_context_unavailable",
          "no_data_sheet",
          "job_queued",
          "library_filter_narrows_route",
          "progress",
          "job_succeeded",
          "rows_unmatched",
          "rows_missing_quantity",
          "rows_need_review",
          "rows_excluded_used_goods",
          "no_data_rows",
          "job_failed"
        ],
        "description": "Stable machine identifier for an end-user message, for translation and for suppressing messages your own UI already covers. Same rule as `ErrorCode`: ignore what you do not recognise."
      },
      "DownloadLinkResponse": {
        "description": "``?as=link`` body of ``GET /v1/mapping-jobs/{job_id}/file`` (spec §4.11).\n\nThe URL carries its own HMAC capability token and needs no bearer header,\nso it can go straight into a browser. It is short-lived by design — hand it\nto the user, never store it.",
        "properties": {
          "download_url": {
            "title": "Download Url",
            "type": "string"
          },
          "expires_at": {
            "format": "date-time",
            "title": "Expires At",
            "type": "string"
          },
          "filename": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "title": "Filename"
          }
        },
        "required": [
          "download_url",
          "expires_at"
        ],
        "title": "DownloadLinkResponse",
        "type": "object"
      },
      "JobProgressEvent": {
        "description": "Payload of the ``snapshot``, ``stage`` and ``progress`` events.\n\nEvery §4.7 field, plus ``message``: the one line to render beside the bar.",
        "properties": {
          "job_id": {
            "format": "uuid",
            "title": "Job Id",
            "type": "string"
          },
          "client_reference": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "title": "Client Reference"
          },
          "status": {
            "enum": [
              "queued",
              "running",
              "succeeded",
              "failed"
            ],
            "title": "Status",
            "type": "string"
          },
          "stage": {
            "anyOf": [
              {
                "enum": [
                  "parsing",
                  "mapping",
                  "auditing"
                ],
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "``parsing`` → ``mapping`` → ``auditing``. ``null`` while queued.",
            "title": "Stage"
          },
          "processed_rows": {
            "default": 0,
            "title": "Processed Rows",
            "type": "integer"
          },
          "total_rows": {
            "default": 0,
            "title": "Total Rows",
            "type": "integer"
          },
          "percent": {
            "default": 0.0,
            "description": "Progress within the current stage. It resets at the parsing→mapping boundary, so it can move backwards.",
            "title": "Percent",
            "type": "number"
          },
          "queue_position": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "title": "Queue Position"
          },
          "submitted_at": {
            "format": "date-time",
            "title": "Submitted At",
            "type": "string"
          },
          "started_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "title": "Started At"
          },
          "finished_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "title": "Finished At"
          },
          "last_progress_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "title": "Last Progress At"
          },
          "elapsed_running_seconds": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "title": "Elapsed Running Seconds"
          },
          "seconds_since_last_progress": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "title": "Seconds Since Last Progress"
          },
          "retry_after_seconds": {
            "default": 15.0,
            "title": "Retry After Seconds",
            "type": "number"
          },
          "error_message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "One end-user sentence on a failed job, ``null`` otherwise. Never the raw exception — see :mod:`open_climate_ai.api.rest.failure`.",
            "title": "Error Message"
          },
          "messages": {
            "items": {
              "$ref": "#/components/schemas/Message"
            },
            "title": "Messages",
            "type": "array"
          },
          "message": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "The first entry of ``messages``, flattened. Present so a progress bar has one string to show without choosing.",
            "title": "Message"
          }
        },
        "required": [
          "job_id",
          "status",
          "submitted_at"
        ],
        "title": "JobProgressEvent",
        "type": "object"
      },
      "JobSucceededEvent": {
        "description": "Terminal ``succeeded`` payload; embeds the full §4.8 result.",
        "properties": {
          "job_id": {
            "format": "uuid",
            "title": "Job Id",
            "type": "string"
          },
          "client_reference": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "title": "Client Reference"
          },
          "status": {
            "const": "succeeded",
            "default": "succeeded",
            "title": "Status",
            "type": "string"
          },
          "result": {
            "$ref": "#/components/schemas/JobResultResponse"
          }
        },
        "required": [
          "job_id",
          "result"
        ],
        "title": "JobSucceededEvent",
        "type": "object"
      },
      "JobFailedEvent": {
        "description": "Terminal ``failed`` payload.\n\n``error`` is the same object a failing request would have returned, down to\nthe ``request_id``, so a CAD-side incident report reads the same whether the\nfailure arrived on a response or on the stream.",
        "properties": {
          "job_id": {
            "format": "uuid",
            "title": "Job Id",
            "type": "string"
          },
          "client_reference": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "title": "Client Reference"
          },
          "status": {
            "const": "failed",
            "default": "failed",
            "title": "Status",
            "type": "string"
          },
          "error": {
            "$ref": "#/components/schemas/ErrorBody"
          },
          "messages": {
            "items": {
              "$ref": "#/components/schemas/Message"
            },
            "title": "Messages",
            "type": "array"
          }
        },
        "required": [
          "job_id",
          "error"
        ],
        "title": "JobFailedEvent",
        "type": "object"
      }
    },
    "securitySchemes": {
      "partnerApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ocai_live_...",
        "description": "A partner key, issued by us per integrating vendor. Send it as\n`Authorization: Bearer ocai_live_...`.\n\nIt identifies your organisation, not a person, so it must be paired with\n`X-OCAI-Subject` naming the customer you are acting for. On its own it is\nrejected with `401`.\n\nKeep it server-side. A key in a browser bundle or a mobile binary is a key you\nhave published."
      },
      "subjectHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-OCAI-Subject",
        "description": "Which of your customers this request is for. Required with a partner key,\nignored with any other credential.\n\nAny stable opaque string: your own customer id, or the end user's email address.\nIt is not a credential and it is not checked against a directory. It is the\nscope boundary: uploads, jobs and results are visible only to the subject that\ncreated them, so sending a different value for the same customer hides their own\nhistory from them."
      },
      "userApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ocai_live_...",
        "description": "A single-tenant key that carries its own identity. Send it as\n`Authorization: Bearer ocai_live_...`, with no `X-OCAI-Subject`.\n\nSame prefix as a partner key; the difference is what the key is bound to, which\nis decided when we issue it. Use this for a script or an internal job that only\never acts for one account."
      },
      "oauth2": {
        "type": "oauth2",
        "description": "An access token from our Keycloak realm, for applications where a real person is\nsigning in. Send it as `Authorization: Bearer <jwt>`.\n\nThe token must be audience-bound to this resource server, which means requesting\nit for this resource rather than reusing one minted for something else.\n\nThe **Authorize** button on this page runs this flow: it signs you in (or\nregisters you) and lets the Test Request console call the API as you. Reads\nwork immediately; a newly self-registered account must be approved before it\ncan start mapping jobs — write to hello@open-climate.ai and we will enable it.\nAccounts created through a partner integration are approved already.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://auth.open-climate.ai/realms/ocai/protocol/openid-connect/auth",
            "tokenUrl": "https://auth.open-climate.ai/realms/ocai/protocol/openid-connect/token",
            "scopes": {
              "openid": "Establish who the signed-in person is.",
              "mcp:tools": "Act on that person's ledgers and jobs."
            },
            "x-scalar-client-id": "ocai-docs",
            "x-usePkce": "SHA-256"
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "uploads",
      "description": "Get a ledger onto the server and find out what shape it is. Two ways in: one authenticated request from your backend, or a single-use URL your user's browser posts to directly."
    },
    {
      "name": "libraries",
      "description": "The emission-factor libraries a job may search, with the publisher codes you pass back in `libraries`."
    },
    {
      "name": "mapping-jobs",
      "description": "Queue a run, then watch it. Mapping is asynchronous because it is minutes of work, so every job answers `202` and reports progress on a stream or a poll."
    },
    {
      "name": "results",
      "description": "Dashboard statistics for a finished job, and the workbook download."
    },
    {
      "name": "rows",
      "description": "The matched lines themselves, as JSON for a UI or as CSV for a pipeline."
    },
    {
      "name": "factors",
      "description": "The anonymous emission-factor catalogue under `/factors/api`: browse, read one factor, list the libraries. No credential; anonymous browsing is metered at ten searches per session."
    }
  ],
  "security": [
    {
      "partnerApiKey": [],
      "subjectHeader": []
    },
    {
      "userApiKey": []
    },
    {
      "oauth2": []
    }
  ]
}
