{
  "info": {
    "name": "Sirveil Exposure API",
    "description": "Two operations, at two very different scales.\n\n`POST /api/v1/verify` answers one question in about a second: **is this identity indexed on\nthis domain?**\n\n`POST /api/v1/scan` runs the full pipeline — search planning, retrieval across the broker\nregistry, breach and public-record lanes, entity resolution and ranking — and returns every\nexposure it can attribute to the identity. It takes **roughly a minute** and sometimes\nseveral; see that operation for the timeout you must configure.\n\n### The verify contract is three states, not five\n\n`indexed`, `not_indexed`, `indeterminate`.\n\nAn exposure product eventually wants two more — `indexed_not_live` and `not_indexed_live` —\nwhich distinguish \"the broker still serves this page\" from \"the search engine has a stale\nentry for a page that now 404s\". Telling those apart requires fetching the page. We have not\nbuilt confirm-fetch, so we do not return those states, because doing so would assert a fetch\nwe never performed. Every response repeats this in `contract.limitation`.\n\n`indeterminate` is a real answer and not an error. It means the search succeeded and the\nevidence was too weak to call either way. A caller that treats it as `not_indexed` is\nreporting an absence we did not observe.\n\n### Versions ship inside the response\n\nEvery response carries `pipeline_version`, `linkage_weights_version` and\n`calibration_version`, on errors as well as successes, so any answer can be reproduced\nagainst the build that produced it.\n\nWhat you were charged is on your usage record and your invoice, not in the response body.\nA response body is not an accounting document: it is produced before the call is metered,\nand a figure printed there would be the one number a caller could not reconcile against\nanything.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "Is this identity indexed on this domain?",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Authorization",
            "value": "Bearer {{apiKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/verify",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "verify"
          ]
        },
        "description": "Issues one `site:`-scoped search against the named domain, normalises the results,\nmatches them against the declared identity and returns a state with its evidence.\n\nTypical latency is 1–2 seconds (p95 measured at 1,463 ms).\n\nThe call performs **no database write**: no profile, no dossier, no stored result. The\nnarrower true statement about the identity itself is that it is used to build the search\nquery and is held in an in-memory result cache, partitioned to your account, for a few\nminutes so that a repeated question is answered consistently. It is not written to disk\nand not retained beyond that window.\n\n**A cache hit is billed and consumes quota exactly like a fresh call.** The cache makes a\nrepeat answer *consistent*, not *free* — what it saves is our provider cost, not your\ncharge. It is also per-instance and short-lived (15 minutes, 800 entries, in the memory of\none warm process), so whether a repeat is served from it is not something you can predict\nor rely on. Do not build a cost model around it.",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"identity\": {\n    \"firstName\": \"Jane\",\n    \"lastName\": \"Doe\",\n    \"email\": \"jane.doe@example.com\",\n    \"city\": \"Austin\",\n    \"state\": \"TX\"\n  },\n  \"domain\": \"whitepages.com\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        }
      },
      "response": [
        {
          "name": "200 — indexed — a matching listing was found",
          "originalRequest": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/verify",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "verify"
              ]
            },
            "description": "Issues one `site:`-scoped search against the named domain, normalises the results,\nmatches them against the declared identity and returns a state with its evidence.\n\nTypical latency is 1–2 seconds (p95 measured at 1,463 ms).\n\nThe call performs **no database write**: no profile, no dossier, no stored result. The\nnarrower true statement about the identity itself is that it is used to build the search\nquery and is held in an in-memory result cache, partitioned to your account, for a few\nminutes so that a repeated question is answered consistently. It is not written to disk\nand not retained beyond that window.\n\n**A cache hit is billed and consumes quota exactly like a fresh call.** The cache makes a\nrepeat answer *consistent*, not *free* — what it saves is our provider cost, not your\ncharge. It is also per-instance and short-lived (15 minutes, 800 entries, in the memory of\none warm process), so whether a repeat is served from it is not something you can predict\nor rely on. Do not build a cost model around it.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"identity\": {\n    \"firstName\": \"Jane\",\n    \"lastName\": \"Doe\",\n    \"email\": \"jane.doe@example.com\",\n    \"city\": \"Austin\",\n    \"state\": \"TX\"\n  },\n  \"domain\": \"whitepages.com\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "status": "OK",
          "code": 200,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "cookie": [],
          "body": "{\n  \"state\": \"indexed\",\n  \"observed_at\": \"2026-08-13T09:14:22.104Z\",\n  \"domain\": \"whitepages.com\",\n  \"evidence\": {\n    \"url\": \"https://www.whitepages.com/name/Jane-Doe/Austin-TX/example\",\n    \"title\": \"Jane Doe, Austin TX — Address, Phone, Age\",\n    \"snippet\": \"Jane Doe, age 40s, Austin TX. Current address, phone numbers and relatives.\",\n    \"retrieved_at\": \"2026-08-13T09:14:21.880Z\"\n  },\n  \"confidence\": 0.87,\n  \"calibrated_probability\": 0.91,\n  \"linkage\": {\n    \"bits\": 14.2,\n    \"probability\": 0.94,\n    \"calibrated\": true,\n    \"weights_version\": \"linkage-v1\"\n  },\n  \"explains\": [\n    \"Issued 1 email-scoped query against whitepages.com.\",\n    \"3 of 10 results were on the requested domain.\",\n    \"Matched on first name, last name, city and state.\"\n  ],\n  \"contract\": {\n    \"states\": [\n      \"indexed\",\n      \"not_indexed\",\n      \"indeterminate\"\n    ],\n    \"limitation\": \"THREE STATES ONLY. This endpoint reads a search index; it does not fetch the page. It therefore cannot distinguish an indexed-and-live listing from a stale index entry, and \\\"not_indexed\\\" means absent from the search index — not proven absent from the site. The confirm-fetch states (indexed_not_live, not_indexed_live) are NOT implemented and are never returned.\"\n  },\n  \"pipeline_version\": \"v11.0.0\",\n  \"linkage_weights_version\": \"linkage-v1@2026-08-04T01:49:53.052Z@f3:u6:x1\",\n  \"calibration_version\": \"calibration-v1@2026-08-04T01:43:19.938Z@platt\",\n  \"meta\": {\n    \"identifier_used\": \"email\",\n    \"results_from_domain\": 3,\n    \"results_total\": 10,\n    \"latency_ms\": 1180,\n    \"search_ok\": true\n  }\n}"
        },
        {
          "name": "200 — indeterminate — searched, evidence too weak to call. NOT not_indexed.",
          "originalRequest": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/verify",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "verify"
              ]
            },
            "description": "Issues one `site:`-scoped search against the named domain, normalises the results,\nmatches them against the declared identity and returns a state with its evidence.\n\nTypical latency is 1–2 seconds (p95 measured at 1,463 ms).\n\nThe call performs **no database write**: no profile, no dossier, no stored result. The\nnarrower true statement about the identity itself is that it is used to build the search\nquery and is held in an in-memory result cache, partitioned to your account, for a few\nminutes so that a repeated question is answered consistently. It is not written to disk\nand not retained beyond that window.\n\n**A cache hit is billed and consumes quota exactly like a fresh call.** The cache makes a\nrepeat answer *consistent*, not *free* — what it saves is our provider cost, not your\ncharge. It is also per-instance and short-lived (15 minutes, 800 entries, in the memory of\none warm process), so whether a repeat is served from it is not something you can predict\nor rely on. Do not build a cost model around it.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"identity\": {\n    \"firstName\": \"Jane\",\n    \"lastName\": \"Doe\",\n    \"email\": \"jane.doe@example.com\",\n    \"city\": \"Austin\",\n    \"state\": \"TX\"\n  },\n  \"domain\": \"whitepages.com\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "status": "OK",
          "code": 200,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "cookie": [],
          "body": "{\n  \"state\": \"indeterminate\",\n  \"observed_at\": \"2026-08-13T09:16:05.550Z\",\n  \"domain\": \"spokeo.com\",\n  \"evidence\": {\n    \"url\": \"https://www.spokeo.com/Jane-Doe/Texas/example\",\n    \"title\": \"Jane Doe in Texas — Spokeo\",\n    \"snippet\": \"We found several people named Jane Doe in Texas.\",\n    \"retrieved_at\": \"2026-08-13T09:16:05.201Z\"\n  },\n  \"confidence\": 0.31,\n  \"calibrated_probability\": 0.28,\n  \"linkage\": {\n    \"bits\": 4.1,\n    \"probability\": 0.42,\n    \"calibrated\": true,\n    \"weights_version\": \"linkage-v1\"\n  },\n  \"explains\": [\n    \"Issued 1 name-anchored query against spokeo.com.\",\n    \"1 of 10 results was on the requested domain.\",\n    \"Matched on last name and state only; no corroborating identifier.\",\n    \"Evidence was too weak to call either way. This is NOT `not_indexed`.\"\n  ],\n  \"contract\": {\n    \"states\": [\n      \"indexed\",\n      \"not_indexed\",\n      \"indeterminate\"\n    ],\n    \"limitation\": \"THREE STATES ONLY. This endpoint reads a search index; it does not fetch the page. It therefore cannot distinguish an indexed-and-live listing from a stale index entry, and \\\"not_indexed\\\" means absent from the search index — not proven absent from the site. The confirm-fetch states (indexed_not_live, not_indexed_live) are NOT implemented and are never returned.\"\n  },\n  \"pipeline_version\": \"v11.0.0\",\n  \"linkage_weights_version\": \"linkage-v1@2026-08-04T01:49:53.052Z@f3:u6:x1\",\n  \"calibration_version\": \"calibration-v1@2026-08-04T01:43:19.938Z@platt\",\n  \"meta\": {\n    \"identifier_used\": \"name_only\",\n    \"results_from_domain\": 1,\n    \"results_total\": 10,\n    \"latency_ms\": 1402,\n    \"search_ok\": true\n  }\n}"
        },
        {
          "name": "401 — A revoked or unknown key",
          "originalRequest": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/verify",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "verify"
              ]
            },
            "description": "Issues one `site:`-scoped search against the named domain, normalises the results,\nmatches them against the declared identity and returns a state with its evidence.\n\nTypical latency is 1–2 seconds (p95 measured at 1,463 ms).\n\nThe call performs **no database write**: no profile, no dossier, no stored result. The\nnarrower true statement about the identity itself is that it is used to build the search\nquery and is held in an in-memory result cache, partitioned to your account, for a few\nminutes so that a repeated question is answered consistently. It is not written to disk\nand not retained beyond that window.\n\n**A cache hit is billed and consumes quota exactly like a fresh call.** The cache makes a\nrepeat answer *consistent*, not *free* — what it saves is our provider cost, not your\ncharge. It is also per-instance and short-lived (15 minutes, 800 entries, in the memory of\none warm process), so whether a repeat is served from it is not something you can predict\nor rely on. Do not build a cost model around it.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"identity\": {\n    \"firstName\": \"Jane\",\n    \"lastName\": \"Doe\",\n    \"email\": \"jane.doe@example.com\",\n    \"city\": \"Austin\",\n    \"state\": \"TX\"\n  },\n  \"domain\": \"whitepages.com\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "status": "Unauthorized",
          "code": 401,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "cookie": [],
          "body": "{\n  \"error\": \"invalid_key\",\n  \"detail\": \"That API key is not recognised, or it has been revoked.\",\n  \"pipeline_version\": \"v11.0.0\",\n  \"linkage_weights_version\": \"linkage-v1@2026-08-04T01:49:53.052Z@f3:u6:x1\",\n  \"calibration_version\": \"calibration-v1@2026-08-04T01:43:19.938Z@platt\"\n}"
        },
        {
          "name": "429 — The plan window is used up. Retry-After is the seconds until it turns over.",
          "originalRequest": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/verify",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "verify"
              ]
            },
            "description": "Issues one `site:`-scoped search against the named domain, normalises the results,\nmatches them against the declared identity and returns a state with its evidence.\n\nTypical latency is 1–2 seconds (p95 measured at 1,463 ms).\n\nThe call performs **no database write**: no profile, no dossier, no stored result. The\nnarrower true statement about the identity itself is that it is used to build the search\nquery and is held in an in-memory result cache, partitioned to your account, for a few\nminutes so that a repeated question is answered consistently. It is not written to disk\nand not retained beyond that window.\n\n**A cache hit is billed and consumes quota exactly like a fresh call.** The cache makes a\nrepeat answer *consistent*, not *free* — what it saves is our provider cost, not your\ncharge. It is also per-instance and short-lived (15 minutes, 800 entries, in the memory of\none warm process), so whether a repeat is served from it is not something you can predict\nor rely on. Do not build a cost model around it.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"identity\": {\n    \"firstName\": \"Jane\",\n    \"lastName\": \"Doe\",\n    \"email\": \"jane.doe@example.com\",\n    \"city\": \"Austin\",\n    \"state\": \"TX\"\n  },\n  \"domain\": \"whitepages.com\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "status": "Too Many Requests",
          "code": 429,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            },
            {
              "key": "Retry-After",
              "value": "3600"
            }
          ],
          "cookie": [],
          "body": "{\n  \"error\": \"quota_exceeded\",\n  \"detail\": \"Monthly quota of 14000 units reached on the Developer plan — 14000 used, 0 remaining, and this call costs 1 unit. Resets 2026-09-01T00:00:00.000Z. Upgrade to continue.\",\n  \"pipeline_version\": \"v11.0.0\",\n  \"linkage_weights_version\": \"linkage-v1@2026-08-04T01:49:53.052Z@f3:u6:x1\",\n  \"calibration_version\": \"calibration-v1@2026-08-04T01:43:19.938Z@platt\"\n}"
        }
      ]
    },
    {
      "name": "Find every exposure attributable to this identity.",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Authorization",
            "value": "Bearer {{apiKey}}"
          },
          {
            "key": "Prefer",
            "value": "respond-async",
            "description": "Send `respond-async` (RFC 7240) to submit the scan as a job and receive 202 with a `job_id` instead of waiting. Omit it for the synchronous shape, which is the default and is unchanged.",
            "disabled": true
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/scan",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "scan"
          ]
        },
        "description": "Runs the full pipeline: builds a profile from what you supply, plans and issues searches\nacross the data-broker registry and the open web, checks breach and public-record lanes,\nresolves candidates to the identity, ranks them, and returns one card per distinct\nexposure with the fields that exposure reveals and the URL each field was read from.\n\n### Configure your client timeout before you call this\n\nMeasured wall clock **on this endpoint**: **135 / 144 / 154 / 155 / 203 seconds**\n(n=5, median 154). The internal pipeline measures faster — 46 / 61 / 66 / 74 / 118, median\n66 — and that figure is quoted here only to be explicit that it is NOT what you should\nplan against: a public scan stores nothing, so it starts cold every time. Set your client\nread timeout to **at least 240 seconds**. PowerShell's `Invoke-RestMethod` defaults to\n**100 seconds**, which is below the measured median of 154 — that is the exact failure\nmeasured on go-live day: the client saw a network error, the server served and billed.\nThe defaults in `axios` (0, but 5s in many wrappers), Python `requests` behind a session\nadapter, and most fetch wrappers will abort mid-scan the same way. A dropped connection\nis not a refund. If your timeout is under 240 seconds, send `Prefer: respond-async`.\n\nIf your caller sits behind an API gateway with a fixed response budget, the synchronous\nshape will not fit and you must use the asynchronous shape below. The two ceilings that\nmatter, from each vendor's own documentation rather than from our estimate:\n\n- **RapidAPI** terminates at **180 seconds** and returns 504. That is both the default and\n  the maximum a provider may configure. Our median of 154 s fits; our measured maximum of\n  203 s does not, so the synchronous shape would 504 on the slow tail.\n- **AWS API Gateway** defaults to **29 seconds**, raisable to at most **300 seconds** by\n  Service Quotas request, and only for Regional and private REST APIs — HTTP APIs and\n  edge-optimized APIs cannot be raised at all.\n\nA 504 from a gateway is not a refund: the scan runs to completion on our side and is\nbilled. Use the asynchronous shape and the question does not arise.\n\n### Asynchronous: submit now, collect later\n\nSend `Prefer: respond-async` and you get **202** immediately with a `job_id` and\na `poll_url`. Your quota, rate limit and spend ceiling are all evaluated at submit, so a\n202 means the work is accepted and will be attempted; a refusal happens there and then,\nbefore anything is queued and before anything is charged.\n\nThen `GET /api/v1/jobs/{job_id}` until `status` is `succeeded` or `failed`. On success the\nresponse carries a `result` object that is **exactly** the body the synchronous shape\nreturns. Polling is free: it consumes no plan units and costs nothing.\n\nEvery job response publishes the current `queue` depth, your `position` in it, an\n`estimated_seconds`, and this deployment's `throughput` — so the wait is a number you can\nread rather than one you discover. Present capacity is **58 scans/hour**;\nsee `x-sirveil-async` for the constraint that sets it.\n\n**Retention.** The identity you submitted and the result are kept for 24 hours\nso you can collect them, then both are deleted and the job reports `expired`. The job\nrecord itself survives with its timings, so a late poll tells you what happened rather\nthan 404-ing. The synchronous shape still stores nothing at all.\n\n### Streaming\n\nSend `Accept: application/x-ndjson` and the response is newline-delimited JSON:\n`{\"type\":\"progress\",\"phase\":\"searching\",\"step\":4,\"steps\":7,\"at\":...}` while the scan runs,\nthen a single `{\"type\":\"result\",\"result\":{...}}` carrying **exactly** the body the\nsynchronous shape returns. On failure after the stream has opened you receive\n`{\"type\":\"error\",\"status\":503,\"error\":\"search_unavailable\",...}` — the HTTP status is\nalready 200 by then, so `status` carries the code the synchronous shape would have used.\n\nValidation and the search-availability preflight both run BEFORE the stream opens, so a\nbad request or an unavailable backend is still a real 400/503 with a JSON body.\n\n### What a scan does not do\n\nIt accumulates nothing across calls. There is no\ndossier, no history, and no `times_seen`: two scans of the same person are two\nindependent observations, and nothing from an earlier call is carried forward. The\nsynchronous shape performs **no database write** at all; the asynchronous shape has to\nhold your input and your answer for the 24-hour retention window described above, and\nthat is the only difference. It also does not fetch and confirm each listing — a card means\n\"this record is indexed and matches the identity you declared\", not \"this page is live\nnow\". Both caveats ship inside every response in `contract.limitation`.\n\nAn empty `cards` array is not proof of absence. Check `search_ok` and `degraded` first.",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"identity\": {\n    \"firstName\": \"Jane\",\n    \"lastName\": \"Doe\",\n    \"city\": \"Austin\",\n    \"state\": \"TX\",\n    \"postalCode\": \"78701\",\n    \"email\": \"jane.doe@example.com\"\n  }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        }
      },
      "response": [
        {
          "name": "202 — Queued. Poll `poll_url` until `status` is `succeeded` or `failed`.",
          "originalRequest": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}"
              },
              {
                "key": "Prefer",
                "value": "respond-async",
                "description": "Send `respond-async` (RFC 7240) to submit the scan as a job and receive 202 with a `job_id` instead of waiting. Omit it for the synchronous shape, which is the default and is unchanged.",
                "disabled": false
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/scan",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "scan"
              ]
            },
            "description": "Runs the full pipeline: builds a profile from what you supply, plans and issues searches\nacross the data-broker registry and the open web, checks breach and public-record lanes,\nresolves candidates to the identity, ranks them, and returns one card per distinct\nexposure with the fields that exposure reveals and the URL each field was read from.\n\n### Configure your client timeout before you call this\n\nMeasured wall clock **on this endpoint**: **135 / 144 / 154 / 155 / 203 seconds**\n(n=5, median 154). The internal pipeline measures faster — 46 / 61 / 66 / 74 / 118, median\n66 — and that figure is quoted here only to be explicit that it is NOT what you should\nplan against: a public scan stores nothing, so it starts cold every time. Set your client\nread timeout to **at least 240 seconds**. PowerShell's `Invoke-RestMethod` defaults to\n**100 seconds**, which is below the measured median of 154 — that is the exact failure\nmeasured on go-live day: the client saw a network error, the server served and billed.\nThe defaults in `axios` (0, but 5s in many wrappers), Python `requests` behind a session\nadapter, and most fetch wrappers will abort mid-scan the same way. A dropped connection\nis not a refund. If your timeout is under 240 seconds, send `Prefer: respond-async`.\n\nIf your caller sits behind an API gateway with a fixed response budget, the synchronous\nshape will not fit and you must use the asynchronous shape below. The two ceilings that\nmatter, from each vendor's own documentation rather than from our estimate:\n\n- **RapidAPI** terminates at **180 seconds** and returns 504. That is both the default and\n  the maximum a provider may configure. Our median of 154 s fits; our measured maximum of\n  203 s does not, so the synchronous shape would 504 on the slow tail.\n- **AWS API Gateway** defaults to **29 seconds**, raisable to at most **300 seconds** by\n  Service Quotas request, and only for Regional and private REST APIs — HTTP APIs and\n  edge-optimized APIs cannot be raised at all.\n\nA 504 from a gateway is not a refund: the scan runs to completion on our side and is\nbilled. Use the asynchronous shape and the question does not arise.\n\n### Asynchronous: submit now, collect later\n\nSend `Prefer: respond-async` and you get **202** immediately with a `job_id` and\na `poll_url`. Your quota, rate limit and spend ceiling are all evaluated at submit, so a\n202 means the work is accepted and will be attempted; a refusal happens there and then,\nbefore anything is queued and before anything is charged.\n\nThen `GET /api/v1/jobs/{job_id}` until `status` is `succeeded` or `failed`. On success the\nresponse carries a `result` object that is **exactly** the body the synchronous shape\nreturns. Polling is free: it consumes no plan units and costs nothing.\n\nEvery job response publishes the current `queue` depth, your `position` in it, an\n`estimated_seconds`, and this deployment's `throughput` — so the wait is a number you can\nread rather than one you discover. Present capacity is **58 scans/hour**;\nsee `x-sirveil-async` for the constraint that sets it.\n\n**Retention.** The identity you submitted and the result are kept for 24 hours\nso you can collect them, then both are deleted and the job reports `expired`. The job\nrecord itself survives with its timings, so a late poll tells you what happened rather\nthan 404-ing. The synchronous shape still stores nothing at all.\n\n### Streaming\n\nSend `Accept: application/x-ndjson` and the response is newline-delimited JSON:\n`{\"type\":\"progress\",\"phase\":\"searching\",\"step\":4,\"steps\":7,\"at\":...}` while the scan runs,\nthen a single `{\"type\":\"result\",\"result\":{...}}` carrying **exactly** the body the\nsynchronous shape returns. On failure after the stream has opened you receive\n`{\"type\":\"error\",\"status\":503,\"error\":\"search_unavailable\",...}` — the HTTP status is\nalready 200 by then, so `status` carries the code the synchronous shape would have used.\n\nValidation and the search-availability preflight both run BEFORE the stream opens, so a\nbad request or an unavailable backend is still a real 400/503 with a JSON body.\n\n### What a scan does not do\n\nIt accumulates nothing across calls. There is no\ndossier, no history, and no `times_seen`: two scans of the same person are two\nindependent observations, and nothing from an earlier call is carried forward. The\nsynchronous shape performs **no database write** at all; the asynchronous shape has to\nhold your input and your answer for the 24-hour retention window described above, and\nthat is the only difference. It also does not fetch and confirm each listing — a card means\n\"this record is indexed and matches the identity you declared\", not \"this page is live\nnow\". Both caveats ship inside every response in `contract.limitation`.\n\nAn empty `cards` array is not proof of absence. Check `search_ok` and `degraded` first.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"identity\": {\n    \"firstName\": \"Jane\",\n    \"lastName\": \"Doe\",\n    \"city\": \"Austin\",\n    \"state\": \"TX\",\n    \"postalCode\": \"78701\",\n    \"email\": \"jane.doe@example.com\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "status": "Accepted",
          "code": 202,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            },
            {
              "key": "Location",
              "value": "/api/v1/jobs/0f6a1c2e-9d3b-4a71-9c55-3e1b2a4d8f10"
            }
          ],
          "cookie": [],
          "body": "{\n  \"job_id\": \"0f6a1c2e-9d3b-4a71-9c55-3e1b2a4d8f10\",\n  \"status\": \"queued\",\n  \"poll_url\": \"/api/v1/jobs/0f6a1c2e-9d3b-4a71-9c55-3e1b2a4d8f10\",\n  \"estimated_seconds\": 154,\n  \"queue\": {\n    \"depth\": 0,\n    \"position\": 1\n  },\n  \"throughput\": {\n    \"capacity_per_hour\": 58,\n    \"drain_concurrency\": 1,\n    \"scan_seconds_median\": 154,\n    \"scan_seconds_max\": 203,\n    \"result_retention_hours\": 24\n  },\n  \"created_at\": \"2026-08-13T09:20:00.000Z\",\n  \"expires_at\": \"2026-08-14T09:20:00.000Z\",\n  \"pipeline_version\": \"v11.0.0\",\n  \"linkage_weights_version\": \"linkage-v1@2026-08-04T01:49:53.052Z@f3:u6:x1\",\n  \"calibration_version\": \"calibration-v1@2026-08-04T01:43:19.938Z@platt\"\n}"
        },
        {
          "name": "429 — The plan window is used up. Retry-After is the seconds until it turns over.",
          "originalRequest": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}"
              },
              {
                "key": "Prefer",
                "value": "respond-async",
                "description": "Send `respond-async` (RFC 7240) to submit the scan as a job and receive 202 with a `job_id` instead of waiting. Omit it for the synchronous shape, which is the default and is unchanged.",
                "disabled": true
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/scan",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "scan"
              ]
            },
            "description": "Runs the full pipeline: builds a profile from what you supply, plans and issues searches\nacross the data-broker registry and the open web, checks breach and public-record lanes,\nresolves candidates to the identity, ranks them, and returns one card per distinct\nexposure with the fields that exposure reveals and the URL each field was read from.\n\n### Configure your client timeout before you call this\n\nMeasured wall clock **on this endpoint**: **135 / 144 / 154 / 155 / 203 seconds**\n(n=5, median 154). The internal pipeline measures faster — 46 / 61 / 66 / 74 / 118, median\n66 — and that figure is quoted here only to be explicit that it is NOT what you should\nplan against: a public scan stores nothing, so it starts cold every time. Set your client\nread timeout to **at least 240 seconds**. PowerShell's `Invoke-RestMethod` defaults to\n**100 seconds**, which is below the measured median of 154 — that is the exact failure\nmeasured on go-live day: the client saw a network error, the server served and billed.\nThe defaults in `axios` (0, but 5s in many wrappers), Python `requests` behind a session\nadapter, and most fetch wrappers will abort mid-scan the same way. A dropped connection\nis not a refund. If your timeout is under 240 seconds, send `Prefer: respond-async`.\n\nIf your caller sits behind an API gateway with a fixed response budget, the synchronous\nshape will not fit and you must use the asynchronous shape below. The two ceilings that\nmatter, from each vendor's own documentation rather than from our estimate:\n\n- **RapidAPI** terminates at **180 seconds** and returns 504. That is both the default and\n  the maximum a provider may configure. Our median of 154 s fits; our measured maximum of\n  203 s does not, so the synchronous shape would 504 on the slow tail.\n- **AWS API Gateway** defaults to **29 seconds**, raisable to at most **300 seconds** by\n  Service Quotas request, and only for Regional and private REST APIs — HTTP APIs and\n  edge-optimized APIs cannot be raised at all.\n\nA 504 from a gateway is not a refund: the scan runs to completion on our side and is\nbilled. Use the asynchronous shape and the question does not arise.\n\n### Asynchronous: submit now, collect later\n\nSend `Prefer: respond-async` and you get **202** immediately with a `job_id` and\na `poll_url`. Your quota, rate limit and spend ceiling are all evaluated at submit, so a\n202 means the work is accepted and will be attempted; a refusal happens there and then,\nbefore anything is queued and before anything is charged.\n\nThen `GET /api/v1/jobs/{job_id}` until `status` is `succeeded` or `failed`. On success the\nresponse carries a `result` object that is **exactly** the body the synchronous shape\nreturns. Polling is free: it consumes no plan units and costs nothing.\n\nEvery job response publishes the current `queue` depth, your `position` in it, an\n`estimated_seconds`, and this deployment's `throughput` — so the wait is a number you can\nread rather than one you discover. Present capacity is **58 scans/hour**;\nsee `x-sirveil-async` for the constraint that sets it.\n\n**Retention.** The identity you submitted and the result are kept for 24 hours\nso you can collect them, then both are deleted and the job reports `expired`. The job\nrecord itself survives with its timings, so a late poll tells you what happened rather\nthan 404-ing. The synchronous shape still stores nothing at all.\n\n### Streaming\n\nSend `Accept: application/x-ndjson` and the response is newline-delimited JSON:\n`{\"type\":\"progress\",\"phase\":\"searching\",\"step\":4,\"steps\":7,\"at\":...}` while the scan runs,\nthen a single `{\"type\":\"result\",\"result\":{...}}` carrying **exactly** the body the\nsynchronous shape returns. On failure after the stream has opened you receive\n`{\"type\":\"error\",\"status\":503,\"error\":\"search_unavailable\",...}` — the HTTP status is\nalready 200 by then, so `status` carries the code the synchronous shape would have used.\n\nValidation and the search-availability preflight both run BEFORE the stream opens, so a\nbad request or an unavailable backend is still a real 400/503 with a JSON body.\n\n### What a scan does not do\n\nIt accumulates nothing across calls. There is no\ndossier, no history, and no `times_seen`: two scans of the same person are two\nindependent observations, and nothing from an earlier call is carried forward. The\nsynchronous shape performs **no database write** at all; the asynchronous shape has to\nhold your input and your answer for the 24-hour retention window described above, and\nthat is the only difference. It also does not fetch and confirm each listing — a card means\n\"this record is indexed and matches the identity you declared\", not \"this page is live\nnow\". Both caveats ship inside every response in `contract.limitation`.\n\nAn empty `cards` array is not proof of absence. Check `search_ok` and `degraded` first.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"identity\": {\n    \"firstName\": \"Jane\",\n    \"lastName\": \"Doe\",\n    \"city\": \"Austin\",\n    \"state\": \"TX\",\n    \"postalCode\": \"78701\",\n    \"email\": \"jane.doe@example.com\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "status": "Too Many Requests",
          "code": 429,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            },
            {
              "key": "Retry-After",
              "value": "3600"
            }
          ],
          "cookie": [],
          "body": "{\n  \"error\": \"quota_exceeded\",\n  \"detail\": \"Monthly quota of 14000 units reached on the Developer plan — 14000 used, 0 remaining, and this call costs 1 unit. Resets 2026-09-01T00:00:00.000Z. Upgrade to continue.\",\n  \"pipeline_version\": \"v11.0.0\",\n  \"linkage_weights_version\": \"linkage-v1@2026-08-04T01:49:53.052Z@f3:u6:x1\",\n  \"calibration_version\": \"calibration-v1@2026-08-04T01:43:19.938Z@platt\"\n}"
        },
        {
          "name": "503 — The queue is not enabled here. Drop the Prefer header and run it synchronously.",
          "originalRequest": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}"
              },
              {
                "key": "Prefer",
                "value": "respond-async",
                "description": "Send `respond-async` (RFC 7240) to submit the scan as a job and receive 202 with a `job_id` instead of waiting. Omit it for the synchronous shape, which is the default and is unchanged.",
                "disabled": true
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/scan",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "scan"
              ]
            },
            "description": "Runs the full pipeline: builds a profile from what you supply, plans and issues searches\nacross the data-broker registry and the open web, checks breach and public-record lanes,\nresolves candidates to the identity, ranks them, and returns one card per distinct\nexposure with the fields that exposure reveals and the URL each field was read from.\n\n### Configure your client timeout before you call this\n\nMeasured wall clock **on this endpoint**: **135 / 144 / 154 / 155 / 203 seconds**\n(n=5, median 154). The internal pipeline measures faster — 46 / 61 / 66 / 74 / 118, median\n66 — and that figure is quoted here only to be explicit that it is NOT what you should\nplan against: a public scan stores nothing, so it starts cold every time. Set your client\nread timeout to **at least 240 seconds**. PowerShell's `Invoke-RestMethod` defaults to\n**100 seconds**, which is below the measured median of 154 — that is the exact failure\nmeasured on go-live day: the client saw a network error, the server served and billed.\nThe defaults in `axios` (0, but 5s in many wrappers), Python `requests` behind a session\nadapter, and most fetch wrappers will abort mid-scan the same way. A dropped connection\nis not a refund. If your timeout is under 240 seconds, send `Prefer: respond-async`.\n\nIf your caller sits behind an API gateway with a fixed response budget, the synchronous\nshape will not fit and you must use the asynchronous shape below. The two ceilings that\nmatter, from each vendor's own documentation rather than from our estimate:\n\n- **RapidAPI** terminates at **180 seconds** and returns 504. That is both the default and\n  the maximum a provider may configure. Our median of 154 s fits; our measured maximum of\n  203 s does not, so the synchronous shape would 504 on the slow tail.\n- **AWS API Gateway** defaults to **29 seconds**, raisable to at most **300 seconds** by\n  Service Quotas request, and only for Regional and private REST APIs — HTTP APIs and\n  edge-optimized APIs cannot be raised at all.\n\nA 504 from a gateway is not a refund: the scan runs to completion on our side and is\nbilled. Use the asynchronous shape and the question does not arise.\n\n### Asynchronous: submit now, collect later\n\nSend `Prefer: respond-async` and you get **202** immediately with a `job_id` and\na `poll_url`. Your quota, rate limit and spend ceiling are all evaluated at submit, so a\n202 means the work is accepted and will be attempted; a refusal happens there and then,\nbefore anything is queued and before anything is charged.\n\nThen `GET /api/v1/jobs/{job_id}` until `status` is `succeeded` or `failed`. On success the\nresponse carries a `result` object that is **exactly** the body the synchronous shape\nreturns. Polling is free: it consumes no plan units and costs nothing.\n\nEvery job response publishes the current `queue` depth, your `position` in it, an\n`estimated_seconds`, and this deployment's `throughput` — so the wait is a number you can\nread rather than one you discover. Present capacity is **58 scans/hour**;\nsee `x-sirveil-async` for the constraint that sets it.\n\n**Retention.** The identity you submitted and the result are kept for 24 hours\nso you can collect them, then both are deleted and the job reports `expired`. The job\nrecord itself survives with its timings, so a late poll tells you what happened rather\nthan 404-ing. The synchronous shape still stores nothing at all.\n\n### Streaming\n\nSend `Accept: application/x-ndjson` and the response is newline-delimited JSON:\n`{\"type\":\"progress\",\"phase\":\"searching\",\"step\":4,\"steps\":7,\"at\":...}` while the scan runs,\nthen a single `{\"type\":\"result\",\"result\":{...}}` carrying **exactly** the body the\nsynchronous shape returns. On failure after the stream has opened you receive\n`{\"type\":\"error\",\"status\":503,\"error\":\"search_unavailable\",...}` — the HTTP status is\nalready 200 by then, so `status` carries the code the synchronous shape would have used.\n\nValidation and the search-availability preflight both run BEFORE the stream opens, so a\nbad request or an unavailable backend is still a real 400/503 with a JSON body.\n\n### What a scan does not do\n\nIt accumulates nothing across calls. There is no\ndossier, no history, and no `times_seen`: two scans of the same person are two\nindependent observations, and nothing from an earlier call is carried forward. The\nsynchronous shape performs **no database write** at all; the asynchronous shape has to\nhold your input and your answer for the 24-hour retention window described above, and\nthat is the only difference. It also does not fetch and confirm each listing — a card means\n\"this record is indexed and matches the identity you declared\", not \"this page is live\nnow\". Both caveats ship inside every response in `contract.limitation`.\n\nAn empty `cards` array is not proof of absence. Check `search_ok` and `degraded` first.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"identity\": {\n    \"firstName\": \"Jane\",\n    \"lastName\": \"Doe\",\n    \"city\": \"Austin\",\n    \"state\": \"TX\",\n    \"postalCode\": \"78701\",\n    \"email\": \"jane.doe@example.com\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "status": "Service Unavailable",
          "code": 503,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "cookie": [],
          "body": "{\n  \"error\": \"async_not_available\",\n  \"detail\": \"Asynchronous scans are not enabled on this deployment. Nothing was charged. Send the same request without the `Prefer: respond-async` header to run the scan synchronously, and note that a synchronous scan takes about 154 seconds (up to 203).\",\n  \"pipeline_version\": \"v11.0.0\",\n  \"linkage_weights_version\": \"linkage-v1@2026-08-04T01:49:53.052Z@f3:u6:x1\",\n  \"calibration_version\": \"calibration-v1@2026-08-04T01:43:19.938Z@platt\"\n}"
        }
      ]
    },
    {
      "name": "Poll an asynchronous scan.",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Authorization",
            "value": "Bearer {{apiKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/jobs/:job_id",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "jobs",
            ":job_id"
          ],
          "variable": [
            {
              "key": "job_id",
              "value": "",
              "description": "The `job_id` from the 202 response."
            }
          ]
        },
        "description": "Status of a job submitted with `Prefer: respond-async`, and its result once it has one.\n\n`status` is one of `queued`, `claimed` (running), `succeeded`, `failed`, `expired`.\nOn `succeeded` the `result` field carries exactly the body a synchronous scan returns.\n\nWhile a job is waiting, `queue.position` is your 1-based place in the queue and\n`estimated_seconds` is how long the answer should take from now. The queue is shared\nacross all customers, which is why the depth is reported as a whole rather than as your\nshare of it — publishing a per-account depth would give you a confidently wrong estimate.\n\n**Polling is free.** It bills nothing and consumes no plan units, so poll as often as the\nrate limit allows. There is no advantage to polling slowly.\n\n**A job id that is not yours returns 404, not 403.** Deliberately: a 403 would confirm the\nid exists, which turns a job id into a way to ask whether someone else is a customer.\n\nJobs `expired` after 24 hours keep their timings and lose their data. Poll before then."
      },
      "response": [
        {
          "name": "200 — Still waiting. `queue.position` and `estimated_seconds` say how long.",
          "originalRequest": {
            "method": "GET",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/jobs/:job_id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "jobs",
                ":job_id"
              ],
              "variable": [
                {
                  "key": "job_id",
                  "value": "",
                  "description": "The `job_id` from the 202 response."
                }
              ]
            },
            "description": "Status of a job submitted with `Prefer: respond-async`, and its result once it has one.\n\n`status` is one of `queued`, `claimed` (running), `succeeded`, `failed`, `expired`.\nOn `succeeded` the `result` field carries exactly the body a synchronous scan returns.\n\nWhile a job is waiting, `queue.position` is your 1-based place in the queue and\n`estimated_seconds` is how long the answer should take from now. The queue is shared\nacross all customers, which is why the depth is reported as a whole rather than as your\nshare of it — publishing a per-account depth would give you a confidently wrong estimate.\n\n**Polling is free.** It bills nothing and consumes no plan units, so poll as often as the\nrate limit allows. There is no advantage to polling slowly.\n\n**A job id that is not yours returns 404, not 403.** Deliberately: a 403 would confirm the\nid exists, which turns a job id into a way to ask whether someone else is a customer.\n\nJobs `expired` after 24 hours keep their timings and lose their data. Poll before then."
          },
          "status": "OK",
          "code": 200,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "cookie": [],
          "body": "{\n  \"job_id\": \"0f6a1c2e-9d3b-4a71-9c55-3e1b2a4d8f10\",\n  \"status\": \"queued\",\n  \"poll_url\": \"/api/v1/jobs/0f6a1c2e-9d3b-4a71-9c55-3e1b2a4d8f10\",\n  \"created_at\": \"2026-08-13T09:20:00.000Z\",\n  \"started_at\": null,\n  \"finished_at\": null,\n  \"estimated_seconds\": 216,\n  \"queue\": {\n    \"depth\": 1,\n    \"position\": 2\n  },\n  \"throughput\": {\n    \"capacity_per_hour\": 58,\n    \"drain_concurrency\": 1,\n    \"scan_seconds_median\": 154,\n    \"scan_seconds_max\": 203,\n    \"result_retention_hours\": 24\n  },\n  \"pipeline_version\": \"v11.0.0\",\n  \"linkage_weights_version\": \"linkage-v1@2026-08-04T01:49:53.052Z@f3:u6:x1\",\n  \"calibration_version\": \"calibration-v1@2026-08-04T01:43:19.938Z@platt\"\n}"
        },
        {
          "name": "200 — `result` is byte-identical to a synchronous scan body. One card shown; a real scan returns as many as it finds.",
          "originalRequest": {
            "method": "GET",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/jobs/:job_id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "jobs",
                ":job_id"
              ],
              "variable": [
                {
                  "key": "job_id",
                  "value": "",
                  "description": "The `job_id` from the 202 response."
                }
              ]
            },
            "description": "Status of a job submitted with `Prefer: respond-async`, and its result once it has one.\n\n`status` is one of `queued`, `claimed` (running), `succeeded`, `failed`, `expired`.\nOn `succeeded` the `result` field carries exactly the body a synchronous scan returns.\n\nWhile a job is waiting, `queue.position` is your 1-based place in the queue and\n`estimated_seconds` is how long the answer should take from now. The queue is shared\nacross all customers, which is why the depth is reported as a whole rather than as your\nshare of it — publishing a per-account depth would give you a confidently wrong estimate.\n\n**Polling is free.** It bills nothing and consumes no plan units, so poll as often as the\nrate limit allows. There is no advantage to polling slowly.\n\n**A job id that is not yours returns 404, not 403.** Deliberately: a 403 would confirm the\nid exists, which turns a job id into a way to ask whether someone else is a customer.\n\nJobs `expired` after 24 hours keep their timings and lose their data. Poll before then."
          },
          "status": "OK",
          "code": 200,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "cookie": [],
          "body": "{\n  \"job_id\": \"0f6a1c2e-9d3b-4a71-9c55-3e1b2a4d8f10\",\n  \"status\": \"succeeded\",\n  \"poll_url\": \"/api/v1/jobs/0f6a1c2e-9d3b-4a71-9c55-3e1b2a4d8f10\",\n  \"created_at\": \"2026-08-13T09:20:00.000Z\",\n  \"started_at\": \"2026-08-13T09:21:03.000Z\",\n  \"finished_at\": \"2026-08-13T09:23:37.000Z\",\n  \"throughput\": {\n    \"capacity_per_hour\": 58,\n    \"drain_concurrency\": 1,\n    \"scan_seconds_median\": 154,\n    \"scan_seconds_max\": 203,\n    \"result_retention_hours\": 24\n  },\n  \"result\": {\n    \"scan_id\": \"b31d9e77-2f04-4c8a-8d61-6a0f5c7e2b19\",\n    \"observed_at\": \"2026-08-13T09:23:37.000Z\",\n    \"latency_ms\": 154000,\n    \"cards\": [\n      {\n        \"id\": \"card_1\",\n        \"rank\": 1,\n        \"tier\": \"A\",\n        \"data_type\": \"person_record\",\n        \"risk_category\": \"identity_theft\",\n        \"why_it_matters\": \"A people-search listing that ties your name to a current address and phone number is the starting point for most account-takeover attempts.\",\n        \"source\": \"example-broker.com\",\n        \"raw_data\": {\n          \"title\": \"Jane Doe, Austin TX — Address & Phone\",\n          \"snippet\": \"Jane Doe, Austin TX. Current address, phone numbers and relatives.\",\n          \"link\": \"https://example-broker.com/p/jane-doe-austin-tx\"\n        },\n        \"exposed_fields\": [\n          {\n            \"kind\": \"address\",\n            \"label\": \"Address\",\n            \"value\": \"1200 Example St, Austin, TX 78701\",\n            \"evidence\": \"declared\",\n            \"source_ref\": \"https://example-broker.com/p/jane-doe-austin-tx\"\n          },\n          {\n            \"kind\": \"age\",\n            \"label\": \"Age\",\n            \"value\": \"40s\",\n            \"evidence\": \"unverified\",\n            \"derived\": \"decade\",\n            \"source_ref\": \"https://example-broker.com/p/jane-doe-austin-tx\"\n          },\n          {\n            \"kind\": \"relative\",\n            \"label\": \"Relative\",\n            \"value\": \"J. D.\",\n            \"evidence\": \"unverified\",\n            \"source_ref\": \"https://example-broker.com/p/jane-doe-austin-tx\"\n          }\n        ],\n        \"sources\": [\n          {\n            \"name\": \"example-broker.com\",\n            \"link\": \"https://example-broker.com/p/jane-doe-austin-tx\"\n          }\n        ],\n        \"also_seen_on\": [\n          {\n            \"name\": \"example-directory.com\",\n            \"link\": \"https://example-directory.com/jane-doe\"\n          }\n        ],\n        \"match_probability\": 0.9,\n        \"match_probability_calibrated\": true\n      }\n    ],\n    \"metrics\": {\n      \"card_count\": 1,\n      \"distinct_exposures\": 1,\n      \"tier_a\": 1,\n      \"tier_b\": 0,\n      \"tier_c\": 0\n    },\n    \"degraded\": false,\n    \"search_ok\": true,\n    \"stopped_early\": false,\n    \"dropped_fields\": [],\n    \"contract\": {\n      \"limitation\": \"A scan reads search indexes and public sources at the moment you call it. It does NOT fetch and confirm every listing, so a card means \\\"this record was indexed and matches the identity you declared\\\", not \\\"this page is live now\\\". Card confidence is a probability, never a proof of identity, and an empty result is not proof of absence — check `search_ok` and `degraded` before reading one as such. Results are computed fresh for this call and not accumulated: two scans of the same person are two independent observations, and nothing from an earlier call is carried forward. If you submit a scan asynchronously (`Prefer: respond-async`), the identity you sent and the result are stored for 24 hours so that you can retrieve them, then both are deleted — the job record itself survives with its timings so a late poll can still tell you what happened. Nothing is carried into a later scan either way.\"\n    },\n    \"pipeline_version\": \"v11.0.0\",\n    \"linkage_weights_version\": \"linkage-v1@2026-08-04T01:49:53.052Z@f3:u6:x1\",\n    \"calibration_version\": \"calibration-v1@2026-08-04T01:43:19.938Z@platt\"\n  },\n  \"pipeline_version\": \"v11.0.0\",\n  \"linkage_weights_version\": \"linkage-v1@2026-08-04T01:49:53.052Z@f3:u6:x1\",\n  \"calibration_version\": \"calibration-v1@2026-08-04T01:43:19.938Z@platt\"\n}"
        },
        {
          "name": "401 — A revoked or unknown key",
          "originalRequest": {
            "method": "GET",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/jobs/:job_id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "jobs",
                ":job_id"
              ],
              "variable": [
                {
                  "key": "job_id",
                  "value": "",
                  "description": "The `job_id` from the 202 response."
                }
              ]
            },
            "description": "Status of a job submitted with `Prefer: respond-async`, and its result once it has one.\n\n`status` is one of `queued`, `claimed` (running), `succeeded`, `failed`, `expired`.\nOn `succeeded` the `result` field carries exactly the body a synchronous scan returns.\n\nWhile a job is waiting, `queue.position` is your 1-based place in the queue and\n`estimated_seconds` is how long the answer should take from now. The queue is shared\nacross all customers, which is why the depth is reported as a whole rather than as your\nshare of it — publishing a per-account depth would give you a confidently wrong estimate.\n\n**Polling is free.** It bills nothing and consumes no plan units, so poll as often as the\nrate limit allows. There is no advantage to polling slowly.\n\n**A job id that is not yours returns 404, not 403.** Deliberately: a 403 would confirm the\nid exists, which turns a job id into a way to ask whether someone else is a customer.\n\nJobs `expired` after 24 hours keep their timings and lose their data. Poll before then."
          },
          "status": "Unauthorized",
          "code": 401,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "cookie": [],
          "body": "{\n  \"error\": \"invalid_key\",\n  \"detail\": \"That API key is not recognised, or it has been revoked.\",\n  \"pipeline_version\": \"v11.0.0\",\n  \"linkage_weights_version\": \"linkage-v1@2026-08-04T01:49:53.052Z@f3:u6:x1\",\n  \"calibration_version\": \"calibration-v1@2026-08-04T01:43:19.938Z@platt\"\n}"
        },
        {
          "name": "404 — Unknown, expired, malformed — or somebody else's. All four look the same.",
          "originalRequest": {
            "method": "GET",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/jobs/:job_id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "jobs",
                ":job_id"
              ],
              "variable": [
                {
                  "key": "job_id",
                  "value": "",
                  "description": "The `job_id` from the 202 response."
                }
              ]
            },
            "description": "Status of a job submitted with `Prefer: respond-async`, and its result once it has one.\n\n`status` is one of `queued`, `claimed` (running), `succeeded`, `failed`, `expired`.\nOn `succeeded` the `result` field carries exactly the body a synchronous scan returns.\n\nWhile a job is waiting, `queue.position` is your 1-based place in the queue and\n`estimated_seconds` is how long the answer should take from now. The queue is shared\nacross all customers, which is why the depth is reported as a whole rather than as your\nshare of it — publishing a per-account depth would give you a confidently wrong estimate.\n\n**Polling is free.** It bills nothing and consumes no plan units, so poll as often as the\nrate limit allows. There is no advantage to polling slowly.\n\n**A job id that is not yours returns 404, not 403.** Deliberately: a 403 would confirm the\nid exists, which turns a job id into a way to ask whether someone else is a customer.\n\nJobs `expired` after 24 hours keep their timings and lose their data. Poll before then."
          },
          "status": "Not Found",
          "code": 404,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "cookie": [],
          "body": "{\n  \"error\": \"job_not_found\",\n  \"detail\": \"No job with that id. Job ids are scoped to your account, and results are kept for 24 hours.\",\n  \"pipeline_version\": \"v11.0.0\",\n  \"linkage_weights_version\": \"linkage-v1@2026-08-04T01:49:53.052Z@f3:u6:x1\",\n  \"calibration_version\": \"calibration-v1@2026-08-04T01:43:19.938Z@platt\"\n}"
        },
        {
          "name": "429 — The plan window is used up. Retry-After is the seconds until it turns over.",
          "originalRequest": {
            "method": "GET",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/jobs/:job_id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "jobs",
                ":job_id"
              ],
              "variable": [
                {
                  "key": "job_id",
                  "value": "",
                  "description": "The `job_id` from the 202 response."
                }
              ]
            },
            "description": "Status of a job submitted with `Prefer: respond-async`, and its result once it has one.\n\n`status` is one of `queued`, `claimed` (running), `succeeded`, `failed`, `expired`.\nOn `succeeded` the `result` field carries exactly the body a synchronous scan returns.\n\nWhile a job is waiting, `queue.position` is your 1-based place in the queue and\n`estimated_seconds` is how long the answer should take from now. The queue is shared\nacross all customers, which is why the depth is reported as a whole rather than as your\nshare of it — publishing a per-account depth would give you a confidently wrong estimate.\n\n**Polling is free.** It bills nothing and consumes no plan units, so poll as often as the\nrate limit allows. There is no advantage to polling slowly.\n\n**A job id that is not yours returns 404, not 403.** Deliberately: a 403 would confirm the\nid exists, which turns a job id into a way to ask whether someone else is a customer.\n\nJobs `expired` after 24 hours keep their timings and lose their data. Poll before then."
          },
          "status": "Too Many Requests",
          "code": 429,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            },
            {
              "key": "Retry-After",
              "value": "3600"
            }
          ],
          "cookie": [],
          "body": "{\n  \"error\": \"quota_exceeded\",\n  \"detail\": \"Monthly quota of 14000 units reached on the Developer plan — 14000 used, 0 remaining, and this call costs 1 unit. Resets 2026-09-01T00:00:00.000Z. Upgrade to continue.\",\n  \"pipeline_version\": \"v11.0.0\",\n  \"linkage_weights_version\": \"linkage-v1@2026-08-04T01:49:53.052Z@f3:u6:x1\",\n  \"calibration_version\": \"calibration-v1@2026-08-04T01:43:19.938Z@platt\"\n}"
        }
      ]
    },
    {
      "name": "Check a key, and see what is left on it.",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Authorization",
            "value": "Bearer {{apiKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/whoami",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "whoami"
          ]
        },
        "description": "Returns the account this key belongs to, its plan, the units left in the current quota\nwindow, the headroom left under the spend ceiling, and which billing rail owns the\naccount.\n\n**This call is free.** It consumes no plan units, makes no search-provider call and\ncosts nothing. Use it as a connection test, as a health check, and to read your own\nremaining quota before submitting a batch — there is no reason to ration it beyond the\nper-minute rate limit, which does apply.\n\nBecause it spends nothing, the quota and spend-ceiling refusals cannot fire on it: an\naccount that is out of units still gets a 200 here. That is deliberate. A customer at\ntheir ceiling is exactly the customer who needs to be able to ask what their ceiling is.\n\nIt reports nothing about any other account, and nothing about our costs."
      },
      "response": [
        {
          "name": "200 — A Developer-plan account, part way through its window",
          "originalRequest": {
            "method": "GET",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/whoami",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "whoami"
              ]
            },
            "description": "Returns the account this key belongs to, its plan, the units left in the current quota\nwindow, the headroom left under the spend ceiling, and which billing rail owns the\naccount.\n\n**This call is free.** It consumes no plan units, makes no search-provider call and\ncosts nothing. Use it as a connection test, as a health check, and to read your own\nremaining quota before submitting a batch — there is no reason to ration it beyond the\nper-minute rate limit, which does apply.\n\nBecause it spends nothing, the quota and spend-ceiling refusals cannot fire on it: an\naccount that is out of units still gets a 200 here. That is deliberate. A customer at\ntheir ceiling is exactly the customer who needs to be able to ask what their ceiling is.\n\nIt reports nothing about any other account, and nothing about our costs."
          },
          "status": "OK",
          "code": 200,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "cookie": [],
          "body": "{\n  \"tenant\": {\n    \"id\": \"3f2b8c10-4a5d-4f7e-9b21-8c6d0e4a1b33\",\n    \"name\": \"Acme Integrations\"\n  },\n  \"plan\": {\n    \"id\": \"developer\",\n    \"label\": \"Developer\"\n  },\n  \"quota\": {\n    \"included_units\": 14000,\n    \"used_units\": 412,\n    \"remaining_units\": 13588,\n    \"window_resets_at\": \"2026-09-01T00:00:00.000Z\"\n  },\n  \"spend_ceiling\": {\n    \"ceiling_usd\": 25,\n    \"spent_usd\": 4.12,\n    \"remaining_usd\": 20.88\n  },\n  \"billing_rail\": \"stripe\",\n  \"channel\": \"api_key\",\n  \"rate_limit_per_minute\": 60,\n  \"pipeline_version\": \"v11.0.0\",\n  \"linkage_weights_version\": \"linkage-v1@2026-08-04T01:49:53.052Z@f3:u6:x1\",\n  \"calibration_version\": \"calibration-v1@2026-08-04T01:43:19.938Z@platt\"\n}"
        },
        {
          "name": "401 — A revoked or unknown key",
          "originalRequest": {
            "method": "GET",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/whoami",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "whoami"
              ]
            },
            "description": "Returns the account this key belongs to, its plan, the units left in the current quota\nwindow, the headroom left under the spend ceiling, and which billing rail owns the\naccount.\n\n**This call is free.** It consumes no plan units, makes no search-provider call and\ncosts nothing. Use it as a connection test, as a health check, and to read your own\nremaining quota before submitting a batch — there is no reason to ration it beyond the\nper-minute rate limit, which does apply.\n\nBecause it spends nothing, the quota and spend-ceiling refusals cannot fire on it: an\naccount that is out of units still gets a 200 here. That is deliberate. A customer at\ntheir ceiling is exactly the customer who needs to be able to ask what their ceiling is.\n\nIt reports nothing about any other account, and nothing about our costs."
          },
          "status": "Unauthorized",
          "code": 401,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "cookie": [],
          "body": "{\n  \"error\": \"invalid_key\",\n  \"detail\": \"That API key is not recognised, or it has been revoked.\",\n  \"pipeline_version\": \"v11.0.0\",\n  \"linkage_weights_version\": \"linkage-v1@2026-08-04T01:49:53.052Z@f3:u6:x1\",\n  \"calibration_version\": \"calibration-v1@2026-08-04T01:43:19.938Z@platt\"\n}"
        }
      ]
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://ai.sirveil.ai",
      "type": "string"
    },
    {
      "key": "apiKey",
      "value": "",
      "type": "string",
      "description": "Your sk_live_ or sk_test_ key. Set this in an environment, not in the collection."
    }
  ]
}