{"components":{"securitySchemes":{"bearerAuth":{"description":"Your PearlAudit API key, e.g. `ta_3f9c…`. Provisioned by your account manager.","scheme":"bearer","type":"http"}}},"info":{"contact":{"email":"help@getpearlaudit.com","name":"PearlAudit","url":"https://pearlaudit.com"},"description":"Property-intelligence API for every parcel in New York City — zoning & FAR, air rights, flood risk, ownership & recorded sales, compliance deadlines, environmental and neighborhood context, and a typeset PDF dossier.\n\n## Authentication\nSend your key as a Bearer token: `Authorization: Bearer ta_…`. Keys are provisioned per contract by your account manager (`help@getpearlaudit.com`).\n\n## Tiers\nThe response is gated to your key's tier: **single** (`$99`, the full report minus comps), **pro/team/enterprise** (everything, incl. nearby-sales comps). A `free` key returns a locked-stub `enrichment`.\n\n## Rate limits\nEach key has a requests-per-minute budget. Every keyed response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`; over budget returns `429` with a `Retry-After` header. Ask your account manager to raise it.\n\n## Errors\nError responses carry a human `error` string AND a stable, machine-branchable `code` your client can switch on (the prose may be reworded; the `code` is stable). Codes include: `unauthorized`, `forbidden`, `tier_forbidden`, `scope_forbidden`, `rate_limited`, `bulk_disabled`, `bulk_cap_reached`, `payload_too_large`, `invalid_request`, `not_found`, `internal`, `monitoring_disabled` (watch surface temporarily paused — retry later), `watch_cap_reached` (watchlist full; `used`/`cap` ride alongside). A key may be **scoped** (least-privilege) to a subset of `query`/`report`/`bulk`; a request outside its scopes returns `403 scope_forbidden`. Every response also carries an `X-Request-Id` header (quote it in support requests); on a `5xx` the same id is in the body as `request_id`.\n\n## Data honesty\nEvery figure is cited to its public source; absent data renders an explicit honest-null, never a guess. This is decision-support, not legal/appraisal advice.","title":"PearlAudit API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/autocomplete":{"get":{"description":"Typeahead address suggestions (each carries the resolved BBL).","operationId":"getAutocomplete","parameters":[{"description":"Partial address.","in":"query","name":"q","required":true,"schema":{"example":"20 west 34","type":"string"}}],"responses":{"200":{"description":"Ranked suggestions."},"401":{"description":"Missing or invalid API key."},"429":{"description":"Per-key rate limit exceeded (see your plan's requests/minute). Retry after the window.","headers":{"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer"}}}}},"summary":"Address autocomplete","tags":["Lookup"]}},"/api/parcel-geojson":{"get":{"description":"A GeoJSON FeatureCollection (lot boundary, building footprint(s), flood zone) for mapping.","operationId":"getParcelGeojson","parameters":[{"in":"query","name":"lat","required":false,"schema":{"format":"double","type":"number"}},{"in":"query","name":"lng","required":false,"schema":{"format":"double","type":"number"}},{"in":"query","name":"bbl","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/geo+json":{}},"description":"GeoJSON FeatureCollection."},"401":{"description":"Missing or invalid API key."},"429":{"description":"Per-key rate limit exceeded (see your plan's requests/minute). Retry after the window.","headers":{"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer"}}}}},"summary":"Parcel + footprint + flood GeoJSON","tags":["Lookup"]}},"/bulk":{"post":{"description":"Screen up to 500 properties in one async job (team tier or higher). Returns a `job_id` to poll. Each successful (`ok`) item costs one credit; `not_found`/`error` are free. Optional `callback_url` (https) receives a signed `bulk.completed` webhook on terminal state. Headers: `X-PearlAudit-Signature: sha256=HMAC-SHA256(secret, raw_body)` (stable v1); `X-PearlAudit-Timestamp` + `X-PearlAudit-Signature-V2: HMAC-SHA256(secret, \"<timestamp>.<raw_body>\")` (verify the timestamp is recent to reject replays); `X-PearlAudit-Event-Id` (unique per delivery — dedupe retries; also in the body as `event_id`). `secret` is returned once in this response; re-fire any terminal job via `POST /bulk/{id}/redeliver`.\n\nSend an optional `Idempotency-Key` header to make retries safe: a repeat submit with the same key + same `items` returns the ORIGINAL job (`status: \"duplicate\"`) instead of creating a second one; a same-key-different-body request returns `409`.","operationId":"createBulk","parameters":[{"description":"Client-chosen key (≤255 chars) making POST /bulk safe to retry.","in":"header","name":"Idempotency-Key","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"example":{"callback_url":"https://hooks.example.com/pearl","items":["1008350041","20 W 34th St, New York, NY"]},"schema":{"properties":{"callback_url":{"description":"Optional https URL for the completion webhook (SSRF-guarded).","format":"uri","type":"string"},"items":{"description":"BBLs or addresses, one per entry.","items":{"type":"string"},"maxItems":500,"type":"array"}},"required":["items"],"type":"object"}}},"required":true},"responses":{"202":{"content":{"application/json":{"example":{"accepted":2,"job_id":"bj_3f…","status":"running","status_url":"/v1/bulk/bj_3f…","webhook":{"configured":true,"secret":"<hex>","verify":"X-PearlAudit-Signature == sha256= HMAC-SHA256(secret, raw_body)"}}}},"description":"Accepted."},"400":{"description":"Invalid or missing input (need one of bbl, lat+lng, or address)."},"401":{"description":"Missing or invalid API key."},"402":{"description":"Monthly bulk allowance reached."},"403":{"description":"Bulk requires the team tier or higher."},"409":{"description":"Idempotency-Key already used with a different request body."},"413":{"description":"Too many items (max 500)."},"429":{"description":"Too many running jobs (max 5), or rate limited."}},"summary":"Bulk screening — submit a batch (async)","tags":["Bulk screening"]}},"/bulk/jobs":{"get":{"description":"Your own recent bulk jobs (slim records, newest first, capped). Owner-scoped to your key.","operationId":"getBulkJobs","responses":{"200":{"content":{"application/json":{"example":{"jobs":[{"created_at":"2026-06-29T14:02:00Z","credits_used":231,"done":250,"finished_at":"2026-06-29T14:09:00Z","job_id":"bj_3f…","status":"done","total":250}]}}},"description":"Recent jobs."},"401":{"description":"Missing or invalid API key."}},"summary":"Bulk screening — your recent jobs","tags":["Bulk screening"]}},"/bulk/usage":{"get":{"description":"Bulk credits used this calendar month against your allowance. API keys minted by your account report the account-wide counter and cap — the same ones job submission enforces; operator-provisioned keys with no owning account are not monthly-capped and return `limit: null, metered: false`.","operationId":"getBulkUsage","responses":{"200":{"content":{"application/json":{"example":{"credits_used":137,"jobs":4,"limit":10000,"metered":true,"period":"2026-06"}}},"description":"Usage."},"401":{"description":"Missing or invalid API key."}},"summary":"Bulk screening — monthly usage","tags":["Bulk screening"]}},"/bulk/{job_id}":{"get":{"description":"Status + per-item results as they finish (owner-scoped to your key/user). For large jobs, poll incrementally: pass `since_idx` = the `cursor` from your last response to fetch only newly-finished items, or `status_only=true` for just the status + whole-job `summary`. The `summary` always reflects the whole job, not the page.","operationId":"getBulk","parameters":[{"in":"path","name":"job_id","required":true,"schema":{"type":"string"}},{"description":"Return only items with idx > since_idx (use the prior response's `cursor`).","in":"query","name":"since_idx","required":false,"schema":{"type":"integer"}},{"description":"Omit the per-item `results` array (status + summary only).","in":"query","name":"status_only","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"example":{"credits_used":2,"done":2,"job_id":"bj_3f…","results":[{"credited":true,"idx":0,"input":"1008350041","status":"ok"}],"status":"done","summary":{"errors":0,"not_in_records":0,"screened":2},"total":2}}},"description":"Job status + results."},"401":{"description":"Missing or invalid API key."},"404":{"description":"No such job (or not yours)."}},"summary":"Bulk screening — poll a job","tags":["Bulk screening"]}},"/bulk/{job_id}/cancel":{"post":{"description":"Marks a running job `canceled`; remaining items are skipped so billing stops and the worker pool frees. Idempotent.","operationId":"cancelBulk","parameters":[{"in":"path","name":"job_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Canceled (or already terminal)."},"401":{"description":"Missing or invalid API key."},"404":{"description":"No such job (or not yours)."}},"summary":"Bulk screening — cancel a job","tags":["Bulk screening"]}},"/bulk/{job_id}/redeliver":{"post":{"description":"Re-deliver the `bulk.completed` webhook for a TERMINAL job, with a fresh `event_id` + signature each time. 409 while still running; 400 if no `callback_url` was configured at submit.","operationId":"redeliverBulk","parameters":[{"in":"path","name":"job_id","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Redelivery queued."},"400":{"description":"No callback_url was configured for this job."},"401":{"description":"Missing or invalid API key."},"404":{"description":"No such job (or not yours)."},"409":{"description":"Job is still running."}},"summary":"Bulk screening — re-fire the completion webhook","tags":["Bulk screening"]}},"/datasets":{"get":{"description":"Public (no key) — for every ingested dataset, the latest recorded version, source vintage, last-updated timestamp, and row count, read from the ingest provenance log. NYC coverage. Absent vintages render null, never a guess.","operationId":"getDatasets","responses":{"200":{"content":{"application/json":{"example":{"as_of":"2026-06-01T00:00:00Z","count":2,"coverage":"New York City (all five boroughs); ~1.1M tax lots + condo units.","datasets":[{"label":"NYC tax-lot records (municipal records)","last_updated":"2026-06-01T00:00:00Z","name":"tax_lot_base","row_count":859000,"source_vintage":"2025 (release 4)","version":12}]}}},"description":"The dataset manifest."},"503":{"description":"The manifest could not be read (`code: manifest_unavailable`). Returned instead of an empty manifest, so a read failure is never published as \"we have no datasets\"."}},"summary":"Dataset freshness & coverage manifest","tags":["Lookup"]}},"/portfolio/summarize":{"post":{"description":"Account-level rollup over a bulk job's stored per-property results (owner-scoped; team tier or higher). `summary` aggregates building-class mix, total open violations, **LL97 carbon $ exposure** (summed MAXIMUM-exposure ceilings per compliance period — a ceiling before credits/mitigation, not a bill), **flood** (parcels in the FEMA SFHA + zone distribution), year-built range, and approximate-match count. `owner_graph` (WS2, additive) is read-time owner-graph intelligence over the SAME submission — who you're dealing with, not just what the lots are: `ownership` (distinct T2-corroborated, `private`-classed recorded owners across the submission + the top owners by lots-in-submission), `common_ownership_clusters` (submitted lots that are BOTH commonly-owned AND recorded-adjacent — an assemblage cluster), and `external_holdings` (for owners holding ≥2 submitted lots, their T2 holding count OUTSIDE this submission + the same aggregated portfolio-health signals the per-property `owner_portfolio` feature shows — counts only, never a per-BBL dump of the owner's other buildings). Every metric carries an honest no-data count (absent data is never assumed clean / zero); `owner_graph` is `null` when the owner-graph dataset isn't loaded or the submission resolved no private T2 owners. Reads already-computed results (no re-screen); no new per-BBL fan-out.","operationId":"portfolioSummarize","requestBody":{"content":{"application/json":{"example":{"job_id":"bj_3f…"},"schema":{"properties":{"job_id":{"type":"string"}},"required":["job_id"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"example":{"job_id":"bj_3f…","job_status":"done","owner_graph":{"common_ownership_clusters":[{"cluster_id":"asm_558a4e…","cluster_size":14,"entity_name":"SALTMEADOW HOLDINGS","lots_in_submission":["4156090001","4156090002","4156090003"]}],"external_holdings":[{"entity_name":"SALTMEADOW HOLDINGS","external_holding_count":20,"lots_in_submission":3,"portfolio_health":{"buildings_evaluated":23,"signals":[{"buildings":6,"key":"open_violations","one_line":"6 of 23 buildings carry ≥1 open DOB/ECB/HPD violation"}]}}],"ownership":{"distinct_recorded_owners":34,"resolved_lots":88,"top_recorded_holders":[{"bbls":["4156090001","4156090002","4156090003"],"entity_name":"SALTMEADOW HOLDINGS","lots_in_submission":3}],"unresolved_lots":12}},"summarized":100,"summary":{"approximate_matches":4,"building_class_distribution":{"O4":40,"R6":60},"flood":{"flood_zone_distribution":{"AE":18,"VE":4,"X":76},"parcels_in_sfha":22,"parcels_without_flood_data":2},"ll97_carbon":{"exposure_2024_2029_usd_per_year":184000,"exposure_2030_2034_usd_per_year":2400000,"parcels_over_limit_2030_2034":41,"parcels_without_carbon_data":12},"open_violations":{"parcels_with_open":58,"parcels_without_data":3,"total":312},"parcels":100,"year_built":{"max":2019,"median":1962,"min":1901}}}}},"description":"The portfolio summary."},"401":{"description":"Missing or invalid API key."},"403":{"description":"Requires the team tier or higher."},"404":{"description":"No such job (or not yours)."}},"summary":"Portfolio rollup over a bulk job","tags":["Bulk screening"]}},"/query":{"get":{"description":"Resolve a parcel by `bbl` (preferred), `lat`+`lng`, or `address`, and return its full intelligence object: `parcel`, `zoning` (FAR/bulk with ZR citations), `buildable` (base-zoning FAR + a `developable_far` synthesis — unused development rights with every reducing/qualifying constraint named + cited), and the `enrichment` data-moat object (44 sub-objects incl. `ownership`, `last_sale`, `comps` [Pro], `rate_context` (the U.S. Treasury benchmark now vs at the most recent recorded mortgage — the rate environment, never the loan's own rate), `rent_context` (HUD Small Area Fair Market Rents for the parcel's address-derived ZIP), `solar_potential` (modeled rooftop solar screening estimate — NREL yield × surveyed footprint roof area), `owner_portfolio` [Pro — the owner's other buildings by deterministic name resolution, with aggregated portfolio-health rollups (per-signal counts of buildings with open violations / AEP / tax-lien-sale / recorded evictions / active mortgages)], `assemblage` [Pro — adjacent lots (shared mapped lot line) recorded to the SAME owner by deterministic name + HPD business-address match, with a recent-deed count and a full-cluster map; a recorded-name linkage, never a claim of intent], `opportunity_zone`, `flood_loss`, `freeboard`, `terrain` (local ground slope + USGS-modeled depth to bedrock at the lot's sample point — excavation/foundation context, never a cost figure), `building_systems`, `hpd_complaints` (occupant-reported housing-maintenance complaints over five years with a twelve-month breakdown — reported conditions, never a finding that a violation was issued), `distress_convergence` (co-occurring public-record distress markers), `compliance`, `environmental`, `proximity`, `peer_context`). Tier-gated.","operationId":"getQuery","parameters":[{"description":"Exact NYC BBL — 10-digit numeric (`1008350041`) or borough-letter form (`MN008350041`). Takes precedence over lat/lng/address; no geocoding.","in":"query","name":"bbl","required":false,"schema":{"example":"1008350041","type":"string"}},{"description":"WGS84 latitude (use with `lng`).","in":"query","name":"lat","required":false,"schema":{"example":40.7484,"format":"double","type":"number"}},{"description":"WGS84 longitude (use with `lat`).","in":"query","name":"lng","required":false,"schema":{"example":-73.9857,"format":"double","type":"number"}},{"description":"Street address; geocoded to a parcel.","in":"query","name":"address","required":false,"schema":{"example":"20 W 34th St, New York, NY","type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"enrichment":{"_comment":"44 sub-objects total; many are honest-null when no record exists.","flood_loss":null,"last_sale":{"date":"2013-10-07","doc_type":"Deed","price":111045119},"opportunity_zone":null,"ownership":{"current_owner":"ESRT EMPIRE STATE BUILDING, L.L.C."},"terrain":{"bedrock_depth_ft":38.0,"sample_method":"footprint_centroid","slope_pct":1.6}},"parcel":{"address":"20 WEST 34 STREET","bbl":"MN008350041","bldgclass":"O4","borough":"Manhattan","builtfar":30.79,"lotarea":91351},"zoning":{"citation":"ZR §81-211","commercial_far":15.0,"district_codes":["C5-3"]}}}},"description":"Parcel + zoning + enrichment."},"400":{"description":"Invalid or missing input (need one of bbl, lat+lng, or address)."},"401":{"description":"Missing or invalid API key."},"404":{"description":"No NYC parcel matched the input."},"429":{"description":"Per-key rate limit exceeded (see your plan's requests/minute). Retry after the window.","headers":{"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer"}}}}},"summary":"Property intelligence (JSON)","tags":["Property intelligence"]}},"/report":{"get":{"description":"Same locator as `/query`, but returns a typeset multi-page PDF dossier (`application/pdf`). `theme` selects a premium look (Pro+); `exclude` hides sections by key.","operationId":"getReport","parameters":[{"description":"Exact NYC BBL — 10-digit numeric (`1008350041`) or borough-letter form (`MN008350041`). Takes precedence over lat/lng/address; no geocoding.","in":"query","name":"bbl","required":false,"schema":{"example":"1008350041","type":"string"}},{"description":"WGS84 latitude (use with `lng`).","in":"query","name":"lat","required":false,"schema":{"example":40.7484,"format":"double","type":"number"}},{"description":"WGS84 longitude (use with `lat`).","in":"query","name":"lng","required":false,"schema":{"example":-73.9857,"format":"double","type":"number"}},{"description":"Street address; geocoded to a parcel.","in":"query","name":"address","required":false,"schema":{"example":"20 W 34th St, New York, NY","type":"string"}},{"description":"Premium dossier theme id (Pro+). Aesthetics only — never changes a figure.","in":"query","name":"theme","required":false,"schema":{"type":"string"}},{"description":"Comma-separated section keys to HIDE in the PDF (e.g. `site_plan,ownership`). Core sections can't be hidden.","in":"query","name":"exclude","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/pdf":{"schema":{"format":"binary","type":"string"}}},"description":"The dossier PDF."},"400":{"description":"Invalid or missing input (need one of bbl, lat+lng, or address)."},"401":{"description":"Missing or invalid API key."},"404":{"description":"No NYC parcel matched the input."},"429":{"description":"Per-key rate limit exceeded (see your plan's requests/minute). Retry after the window.","headers":{"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer"}}}}},"summary":"Typeset PDF dossier","tags":["Property intelligence"]}},"/usage":{"get":{"description":"The authenticated caller's OWN usage: this period's dossier meter (app accounts) and bulk credit tally (team tier and higher), against the live effective limits. Never another account's numbers.","operationId":"getUsage","responses":{"200":{"content":{"application/json":{"example":{"bulk":{"credits_used":137,"jobs":4,"limit":10000},"dossiers":{"limit":10000,"reset":"monthly","used":12},"period":"2026-07","tier":"enterprise"}}},"description":"Usage rollup."},"401":{"description":"Missing or invalid API key."}},"summary":"Your usage rollup","tags":["Account"]}},"/usage/activity":{"get":{"description":"The authenticated caller's OWN activity log — every search and dossier, timestamped, newest first, with the `request_id` that also appears in the matching dossier's footer. App accounts include activity from API keys they own (`via: api_key`). Cursor pagination via `before_id` (echoed as `next_before_id`); `?format=csv` downloads the page as CSV for a compliance file. Retention: 365 days.","operationId":"getUsageActivity","parameters":[{"in":"query","name":"limit","schema":{"default":50,"maximum":500,"minimum":1,"type":"integer"}},{"description":"Return rows with id strictly below this (from a prior page's next_before_id).","in":"query","name":"before_id","schema":{"type":"integer"}},{"in":"query","name":"format","schema":{"default":"json","enum":["json","csv"],"type":"string"}}],"responses":{"200":{"content":{"application/json":{"example":{"activity":[{"address":"120 Broadway","event":"report","id":9124,"lat":40.708,"lng":-74.011,"request_id":"a0eaa2fa","status":200,"timestamp":"2026-07-10T14:02:11Z","via":"app"}],"count":1,"next_before_id":null,"retention_days":365}}},"description":"Activity page (JSON, or text/csv when format=csv)."},"401":{"description":"Missing or invalid API key."}},"summary":"Your activity / audit trail","tags":["Account"]}},"/watchlist":{"get":{"operationId":"listWatch","responses":{"200":{"description":"`{items: [{id, bbl, label, address, muted, created_at, event_count, latest_event, latest_event_at, lat, lng, activity_class}], used, cap, enabled}`. `lat`/`lng` are a point on the tax lot (WGS84; null when the lot cannot be placed). `activity_class` classifies the item's events from the past 30 days: `enforcement` | `transactional` | `routine` | `quiet`."},"401":{"description":"Missing or invalid API key."}},"summary":"List watched properties","tags":["Monitoring"]},"post":{"description":"Add a property to your watchlist (team tier or higher; keys need the `watch` scope). We diff the public record on every NYC data refresh — at least weekly for enforcement feeds, monthly for the rest, weekly for recorded deeds/mortgages — and record typed events. First sight is a silent baseline: you are alerted on the NEXT change, never on add. Condo unit BBLs resolve automatically (building-level signals watch the building; the title watch stays on the unit). Slots are capped per plan; 409 when full, 404 for a BBL not in NYC records.","operationId":"addWatch","requestBody":{"content":{"application/json":{"schema":{"properties":{"address":{"type":"string"},"bbl":{"description":"Letter (MN008350041), numeric (1008350041), or dashed form.","type":"string"},"email":{"description":"Alert email (optional; webhook-only integrations can omit).","type":"string"},"label":{"type":"string"}},"required":["bbl"],"type":"object"}}},"required":true},"responses":{"200":{"description":"Already watching (`existing: true`)."},"201":{"description":"Watching. `{ok, id, bbl, used, cap}`."},"401":{"description":"Missing or invalid API key."},"403":{"description":"Plan/scope does not include monitoring (`code: tier_forbidden`)."},"404":{"description":"BBL not in NYC parcel records (`code: not_found`)."},"409":{"description":"Watchlist full (`code: watch_cap_reached`, with `used`/`cap`)."},"429":{"description":"Per-key rate limit exceeded (see your plan's requests/minute). Retry after the window.","headers":{"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer"}}}}},"summary":"Watch a property (monitoring)","tags":["Monitoring"]}},"/watchlist/events":{"get":{"description":"Typed change events on YOUR watched properties, newest first, cursor-paginated. Every summary states the record and cites the source + its as-of date; events reflect when NYC publishes a record, not the moment it happens.","operationId":"watchEvents","parameters":[{"description":"Return events with id < cursor (from the previous page's `next_cursor`).","in":"query","name":"cursor","required":false,"schema":{"type":"integer"}},{"in":"query","name":"limit","required":false,"schema":{"maximum":200,"type":"integer"}}],"responses":{"200":{"description":"`{events: [{id, bbl, label, address, signal, kind, severity, summary, detail, detected_at}], next_cursor}`."},"401":{"description":"Missing or invalid API key."}},"summary":"Monitoring events feed","tags":["Monitoring"]}},"/watchlist/from-bulk":{"post":{"description":"Adds every successfully-screened (`ok`) item of one of YOUR bulk jobs to the watchlist, up to your slot cap — the response is honest about partial fills (`added` / `already_watching` / `skipped_cap` / `unresolvable`). New items baseline silently; alerts start from the next change.","operationId":"watchFromBulk","requestBody":{"content":{"application/json":{"schema":{"properties":{"job_id":{"type":"string"}},"required":["job_id"],"type":"object"}}},"required":true},"responses":{"200":{"description":"`{ok, job_id, ok_items, added, already_watching, skipped_cap, unresolvable, cap}`."},"401":{"description":"Missing or invalid API key."},"404":{"description":"Not your job / unknown."}},"summary":"Watch all properties from a bulk job (portfolio monitoring)","tags":["Monitoring"]}},"/watchlist/prefs":{"get":{"operationId":"getWatchPrefs","responses":{"200":{"description":"`{email, email_enabled, digest, webhook_url}`."},"401":{"description":"Missing or invalid API key."}},"summary":"Read notification prefs","tags":["Monitoring"]},"put":{"description":"Partial update: `email`, `email_enabled`, `digest` (immediate|weekly|monthly), and — team tier or higher — `webhook_url` (https, SSRF-guarded; empty string clears). Setting a NEW webhook_url mints a signing secret returned ONCE as `webhook_secret`. Every event then POSTs a signed `watch.event`: verify `X-PearlAudit-Signature == sha256=HMAC-SHA256(secret, raw_body)` (v1) or the replay-guarded `X-PearlAudit-Timestamp` + `X-PearlAudit-Signature-V2 == HMAC-SHA256(secret, \"<ts>.<raw_body>\")`; `X-PearlAudit-Event-Id` dedupes retries.","operationId":"putWatchPrefs","requestBody":{"content":{"application/json":{"schema":{"properties":{"digest":{"enum":["immediate","weekly","monthly"],"type":"string"},"email":{"type":"string"},"email_enabled":{"type":"boolean"},"webhook_url":{"type":"string"}},"type":"object"}}},"required":true},"responses":{"200":{"description":"Current prefs (+ `webhook_secret` once, when minted)."},"401":{"description":"Missing or invalid API key."},"403":{"description":"Webhooks require team+."}},"summary":"Notification prefs (email digest + enterprise webhook)","tags":["Monitoring"]}},"/watchlist/prefs/signals":{"get":{"description":"Your account-wide delivery lane per monitored signal. Lanes: `immediate` (paid plans), `digest`, `feed` (in-app only), `off`. `effective` is the resolved lane; `auto` means the built-in default (priority records send immediately, the rest batch into your digest). `unmonitored` lists the signals you have turned off — an off signal is still recorded, just not delivered.","operationId":"getSignalPrefs","responses":{"200":{"description":"`{preset, prefs_enabled, entitled_immediate, signals: [{signal, label, rank, account, effective}], unmonitored}`."},"401":{"description":"Missing or invalid API key."}},"summary":"Per-signal delivery lanes (account default)","tags":["Monitoring"]},"put":{"description":"Partial update: `{signals: {\"<signal>\": \"immediate|digest|feed|off|default\"}}` using the public signal names from GET (`default` clears the override so the signal falls back to your account/preset setting). Setting `immediate` requires a plan that includes immediate delivery (403 otherwise — the request is refused, never silently downgraded).","operationId":"putSignalPrefs","requestBody":{"content":{"application/json":{"schema":{"properties":{"signals":{"additionalProperties":{"enum":["immediate","digest","feed","off"],"type":"string"},"type":"object"}},"required":["signals"],"type":"object"}}},"required":true},"responses":{"200":{"description":"The updated grid."},"401":{"description":"Missing or invalid API key."},"403":{"description":"`immediate` requires a plan with immediate delivery."}},"summary":"Update per-signal lanes (account default)","tags":["Monitoring"]}},"/watchlist/preset":{"post":{"description":"Applies a role preset (`everything|lender|manager|developer|owner`) to your account-default lanes. The preset is materialized at pick time — later retunes of the bundles never change what you already receive. On plans without immediate delivery, immediate-lane signals are applied as `digest` and the response says so (`clamped: true`).","operationId":"pickWatchPreset","requestBody":{"content":{"application/json":{"schema":{"properties":{"preset":{"enum":["everything","lender","manager","developer","owner"],"type":"string"}},"required":["preset"],"type":"object"}}},"required":true},"responses":{"200":{"description":"`{ok, preset, clamped, note}`."},"400":{"description":"Unknown preset."},"401":{"description":"Missing or invalid API key."}},"summary":"Apply a monitoring preset","tags":["Monitoring"]}},"/watchlist/{id}":{"delete":{"operationId":"deleteWatch","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Removed."},"401":{"description":"Missing or invalid API key."},"404":{"description":"Not yours / unknown (`code: not_found`)."}},"summary":"Stop watching","tags":["Monitoring"]},"patch":{"description":"Updates the item's `label` without touching anything else — the add date, baselines, and event history are preserved (deleting and re-adding, by contrast, restarts the first-sight window). Null or empty clears the label.","operationId":"renameWatch","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"label":{"description":"Max 120 chars; null/empty clears.","type":"string"}},"type":"object"}}},"required":true},"responses":{"200":{"description":"`{ok, id, label}`."},"401":{"description":"Missing or invalid API key."},"404":{"description":"Not yours / unknown (`code: not_found`)."}},"summary":"Rename a watch (label only)","tags":["Monitoring"]}},"/watchlist/{id}/prefs/signals":{"get":{"operationId":"getWatchSignalPrefs","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"The grid with per-watch overrides (`watch` column)."},"401":{"description":"Missing or invalid API key."},"404":{"description":"Not your watch item."}},"summary":"Per-signal lanes for ONE watch (override)","tags":["Monitoring"]},"put":{"operationId":"putWatchSignalPrefs","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"signals":{"additionalProperties":{"enum":["immediate","digest","feed","off"],"type":"string"},"type":"object"}},"required":["signals"],"type":"object"}}},"required":true},"responses":{"200":{"description":"The updated grid."},"401":{"description":"Missing or invalid API key."},"403":{"description":"`immediate` requires a plan with immediate delivery."},"404":{"description":"Not your watch item."}},"summary":"Update per-signal lanes for ONE watch","tags":["Monitoring"]}}},"security":[{"bearerAuth":[]}],"servers":[{"description":"Production","url":"https://pearlaudit.com/v1"}],"tags":[{"description":"The core — a parcel's full intelligence object as JSON, and the typeset PDF dossier.","name":"Property intelligence"},{"description":"Address autocomplete + parcel GeoJSON for maps.","name":"Lookup"},{"description":"Async batch screening (team tier or higher): submit → poll → cancel, monthly usage, and optional signed completion webhooks.","name":"Bulk screening"}]}