Verity NHD

Developers

Order NHD reports from your own software

A small REST API: place a California Natural Hazard Disclosure order, get told when the PDF is ready, download it. JSON in, JSON out, signed webhooks, and an OpenAPI 3.1 description you can generate a client from.

Base URL
https://veritynhd.com/api/v1
Authentication
Bearer API key, created in the app. Test keys run the full pipeline at no charge.
Webhooks
HMAC-signed events when a report is ready, held, failed or signed — no polling.

Order California Natural Hazard Disclosure reports, check their status and download the PDF from your own software or a transaction platform.

Authentication

Create a key on the Developer API page in the app. Send it as a bearer token on every request:

Authorization: Bearer vnhd_live_…

A key acts as the user who created it, inside that user's workspace, with exactly the permissions that user has in the web app. Keys are shown once, stored hashed, and can be revoked at any time. Test keys (vnhd_test_…, administrators only) place no-charge orders that run the full pipeline; use them to build against the real thing.

Scopes: orders:write (place orders), orders:read (read orders and status), reports:read (download PDFs). New keys have all three.

Rate limit: 120 requests per minute per key. Over the limit you get 429 with a Retry-After header.

Errors

Every error is JSON with a machine-readable code:

{ "error": { "code": "validation_failed", "message": "The request body has problems.", "issues": [ { "path": "property.zip", "message": "String must contain at least 5 character(s)" } ] } }
StatusCodeMeaning
401unauthorized, key_revokedMissing, unknown or revoked key
403insufficient_scopeThe key lacks the scope for this endpoint
404not_foundNo such order in your workspace
409report_not_ready, not_signed_yetThe PDF you asked for does not exist yet
422validation_failed, unknown_product, order_rejectedFix the request; order_rejected carries the same message the web form would show (billing hold, missing escrow details, …)
429rate_limitedSlow down
5xxserver_error, storage_errorOurs; safe to retry

Place an order

POST /orders — scope orders:write

{
  "product": "Standard NHD",
  "property": {
    "address": "1444 Nye Street",
    "city": "San Rafael",
    "state": "CA",
    "zip": "94901",
    "apn": "011-123-45",
    "county": "Marin"
  },
  "parties": {
    "buyer_name": "Ana Buyer", "buyer_email": "ana@example.com",
    "seller_name": "Sam Seller", "seller_email": "sam@example.com"
  },
  "escrow": {
    "company": "Fidelity National Title",
    "officer_name": "Pat Escrow",
    "officer_email": "pat@example.com",
    "phone": "415-555-0100",
    "file_number": "FNT-12345"
  },
  "listing_agent": { "name": "Lee Agent", "email": "lee@example.com" },
  "estimated_close_date": "2026-10-15",
  "billing": "pay_at_close",
  "external_ref": "skyslope-file-477309",
  "idempotency_key": "skyslope-file-477309-nhd",
  "report_recipients": [ { "role": "tc", "name": "Taylor TC", "email": "taylor@example.com" } ]
}

Response 201 Created with the order (below) and a Location header. Generation starts immediately; a report is normally ready in under three minutes.

Read an order

GET /orders/{id} — scope orders:read

{
  "id": "6e6ff50f-9ca4-47c1-98e6-e763667edeee",
  "order_number": "VN-10188",
  "object": "order",
  "created_at": "2026-09-13T23:01:38Z",
  "updated_at": "2026-09-14T16:31:49Z",
  "status": "ordered",
  "external_ref": "skyslope-file-477309",
  "idempotency_key": "skyslope-file-477309-nhd",
  "test": false,
  "product": { "id": "…", "name": "Standard NHD", "price": 74.95 },
  "billing": { "method": "pay_at_close", "status": "unpaid", "amount": 74.95 },
  "property": { "address": "1444 Nye Street", "city": "San Rafael", "state": "CA", "zip": "94901", "apn": "…", "county": "Marin", "owner": null },
  "parties": { "buyer_name": "…", "buyer_email": "…", "seller_name": "…", "seller_email": "…" },
  "escrow": { "company": "…", "officer_name": "…", "officer_email": "…", "file_number": "…" },
  "listing_agent": { "name": "…", "email": "…" },
  "estimated_close_date": "2026-10-15",
  "report": {
    "status": "ready",
    "step": null,
    "step_description": null,
    "error": null,
    "pdf_url": "https://veritynhd.com/api/v1/orders/6e6f…/report.pdf",
    "signed_pdf_url": null,
    "signed_at": null
  },
  "determinations": {
    "special_flood_hazard_area": false,
    "dam_inundation_zone": false,
    "very_high_fire_hazard_severity_zone": false,
    "wildland_state_responsibility_area": false,
    "earthquake_fault_zone": false,
    "seismic_hazard_zone": false,
    "seismic_hazard_zone_map_released": true,
    "high_fire_hazard_severity_zone_local": false,
    "airport_influence_area": false,
    "airport_noise_zone": false,
    "former_military_ordnance_location": false,
    "hazardous_waste_site_cortese_list": false
  },
  "links": { "self": "…/api/v1/orders/6e6f…", "web": "https://veritynhd.com/orders/6e6f…" }
}

report.status is one of:

ValueMeaning
generatingWorking. step / step_description say which stage (property, hazard, environmental, summary, pdf).
readyThe PDF exists; fetch it from report.pdf_url.
waiting_on_government_sourceA statutory government data source is down. The order re-runs automatically when it is back; error names the source.
failedGeneration stopped; error explains. Retry from the web app or contact support.
awaiting_paymentThe order is not paid yet (invite-to-pay flows).

determinations: the hazard yes/no answers. The whole object is null until the hazard step has run; after that, a key is present once it has been determined (the eight statutory zones always are), and a key whose value is null means its government source was unavailable and that line is pending. Keys that do not apply to a product (for example the airport screens on some tiers) are simply absent. seismic_hazard_zone_map_released qualifies seismic_hazard_zone: false means no CGS Seismic Hazard Zone map has been released for the property's quadrangle, so the statutory answer on the disclosure statement is "Map not yet released by state" rather than "No".

If you poll, do so no more often than every 10 seconds — or register a webhook (below) and never poll at all.

List orders

GET /orders?limit=25&external_ref=…&created_before=… — scope orders:read

Returns { "object": "list", "data": [ …orders… ], "has_more": true, "next_created_before": "2026-09-01T…" }. Pass next_created_before back as created_before for the next page. limit is 1–100.

Download the PDF

GET /orders/{id}/report.pdf — scope reports:read

Returns the report as application/pdf. Add ?variant=signed for the executed (signed) rendition once report.signed_at is set. Before the report is ready you get 409 report_not_ready.

Screen an address (no order)

GET /hazards?address=…&city=…&zip=… or GET /hazards?lat=…&lng=… — scope orders:read

The statutory hazard yes/no answers for a property without placing an order — same engine and data as a report, so what you show a client here is what the report will say. It is screening only: the disclosure document is the ordered report.

{
  "object": "hazard_screen",
  "determined_at": "2026-09-15T17:02:11Z",
  "property": {
    "query": "23825 Stuart Ranch Rd, Malibu, CA, 90265",
    "formatted_address": "23825 Stuart Ranch Rd, Malibu, CA 90265, USA",
    "coordinates": { "lat": 34.03969, "lng": -118.6935 }
  },
  "determinations": {
    "special_flood_hazard_area": false,
    "dam_inundation_zone": false,
    "very_high_fire_hazard_severity_zone": true,
    "wildland_state_responsibility_area": false,
    "earthquake_fault_zone": false,
    "seismic_hazard_zone": true,
    "seismic_hazard_zone_map_released": true,
    "high_fire_hazard_severity_zone_local": true
  },
  "details": { "fema_flood_zone": "X", "fire_severity_sra": null, "fire_severity_lra": "Very High", "seismic_zone_types": ["liquefaction"] },
  "sources": { "fhsz_sra": "mirror", "fhsz_lra": "mirror", "alquist_priolo": "mirror", "seismic_liquefaction": "mirror", "seismic_landslide": "mirror", "dam_inundation": "primary", "fema": "primary" },
  "unavailable": [],
  "disclaimer": "Screening only. …"
}

Webhooks

Register an https endpoint on the Developer API page and choose the events you want. We POST a signed JSON event to it; you answer with any 2xx within 10 seconds.

EventWhen
order.createdAn order was placed (through the API or the web app)
order.report_readyThe report PDF exists — fetch it from data.order.report.pdf_url
order.heldA statutory government source is down; the order re-runs automatically when it is back
order.failedGeneration stopped after repeated failures
order.signedAll parties have signed; ?variant=signed is available
webhook.testSent when you press Send test

Event body:

{
  "id": "0f6c…",                       // delivery id — the same across retries; use it to de-duplicate
  "object": "event",
  "event": "order.report_ready",
  "api_version": "2026-09-14",
  "created_at": "2026-09-14T17:49:31Z",
  "data": {
    "order": {
      "id": "e91c…", "order_number": "VN-10190", "status": "ordered",
      "external_ref": "skyslope-file-477309", "test": false,
      "property": { "address": "1444 Nye Street", "city": "San Rafael", "state": "CA", "zip": "94901" },
      "report": { "status": "ready", "step": null, "error": null,
                  "pdf_url": "https://veritynhd.com/api/v1/orders/e91c…/report.pdf",
                  "signed_pdf_url": null, "signed_at": null },
      "updated_at": "2026-09-14T17:49:30Z",
      "links": { "self": "https://veritynhd.com/api/v1/orders/e91c…", "web": "https://veritynhd.com/orders/e91c…" }
    }
  }
}

Events are thin on purpose: fetch links.self with your API key when you need the full order (parties, determinations, billing).

Verifying the signature

Headers on every delivery:

X-Verity-Event: order.report_ready
X-Verity-Delivery: 0f6c…
X-Verity-Signature: t=1757872171,v1=5f1a…   (HMAC-SHA256 of "<t>.<raw body>" with your endpoint's whsec_ secret)
// Node — verify before trusting the body. Use the raw request bytes, not re-serialized JSON.
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;      // replay window
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Delivery and retries

Changelog

Building an integration?

Transaction platforms, brokerage back-offices and escrow systems can create a key today and place test orders against the real pipeline. If you need volume pricing or a partner agreement, talk to us.