Changelog
Customer-visible changes to the PullFirst API — endpoints, response shapes, error envelopes, and authentication.
2026-05-05
Added
/v1/permits/blockand/v1/permits/locations-in-areanow returnworkTypeanddescriptionon every location. Same nullable string fields already exposed on permit list responses — they describe the scope of work recorded by the permit office. Both fields are optional in the schema, so existing parsers that ignore unknown keys are unaffected.
2026-05-01
Added
- Enforcement responses now include a
canonicalCompanyNamefield. A stable normalized grouping key for enforcement records without a confirmed license match — collapses curly and straight apostrophes, trims punctuation, and lowercases — so consumers can group rows by the same contractor name across records that lack a license number. Existing parsers that ignore unknown keys are unaffected.
Fixed
/v1/permits/addressno longer hangs on multi-token queries with short common tokens. Queries like25 SE 13 AVEpreviously triggered a 30-second timeout when ≤2-character tokens fed the per-term match chain. Those tokens are now dropped before matching; results may include rows that don't contain the dropped tokens.
2026-04-30 #2
Highlights: anonymous traffic to public endpoints is now rate-limited per IP, and three permit aggregate endpoints stop timing out under cold cache.
Changed
- License DTOs in aggregate responses now include an
isPrimaryflag. Endpoints that return a list of licenses for a single contractor — most notably the contractor full-profile aggregate — setisPrimary: trueon the canonical license for that contractor. The field is optional with defaultfalse, so existing parsers that ignore unknown keys are unaffected. Use it to highlight the primary license in UI rather than guessing from sort order. /v1/permits/viewport-contractorsonly returns rows tied to a confirmed license match. Rows previously included anonymousContractorName-only entries withnulllicenseNumber; those are now filtered out. Match tiersVERIFIEDandLIKELYqualify. Expect lower row counts and a non-nulllicenseNumberon every result.
Fixed
/v1/permits/density,/v1/permits/stats, and/v1/permits/citiesno longer time out under cold cache. All three are now served from materialized aggregates refreshed daily, eliminating the 15s timeouts that intermittently surfaced as5xxon the first call after a deploy. Response shapes are unchanged./v1/permits/blockno longer times out on the first request after a deploy. A startup warmer pre-compiles the underlying search query shapes so the first real caller hits a warm plan cache. Response shape is unchanged.
Security
- Anonymous traffic to public endpoints is now rate-limited per IP. Unauthenticated callers get 60 requests/minute against the suggest endpoints (
/v1/licenses/suggest,/v1/permits/street-suggest,/v1/permits/address-suggest) and 600/minute overall. Hitting either ceiling returns429withrate_limit.exceededand aRetry-Afterheader in seconds. HonorRetry-Afterbefore retrying. - Authenticated callers are exempt from the per-IP limiter and continue to be metered by their plan tier. Sending a valid bearer key on any public endpoint (not just protected ones) attaches your customer record to the request and routes you through tier-based metering instead of the per-IP bucket.
/v1/licenses/suggestis now cached for 15 minutes per query/limit/filter combination. Same response shape; faster on repeated keystrokes for autocomplete UIs.
2026-04-30
Highlights: two permit endpoints removed.
Removed
/v1/permits/streetsand/v1/permits/street-suggestremoved. These were experimental street-grouping helpers and have been retired with no replacement. Calls now return404 Not Found. If your integration relied on either endpoint, switch to/v1/permitswith appropriate filters or contact us so we can advise.
2026-04-26 #2
Fixed
- Google sign-in works again on the dashboard. The OAuth callback was returning
internal.errorfor most attempts after the production API moved to multiple machines — the cookie that ties the start of sign-in to the callback was being encrypted on one machine and could not be decrypted on another. Sessions created via the dashboard sign-in flow now succeed and persist; any session-authenticated API access tied to those sessions is restored.
2026-04-26
Highlights: two new permits endpoints (heatmap and stats) and a year filter on the cities endpoint.
Added
/v1/permits/heatmapreturns aggregated permit density with the full filter set. Accepts the same filters as/v1/permits(from,to,year,city,permitType,contractorName,matchTier) plus aweightparameter that switches the aggregation between permit count (weight=count, default) and summed declared value (weight=value). An unfiltered call mirrors/v1/permits/density./v1/permits/statsreturns aggregate permit statistics in one call. Totals, issued-date span, summed declared value, geocoding coverage, distinct city count, and top permit types.
Changed
/v1/permits/citiesaccepts ayearquery parameter. Restricts the GROUP BY to permits issued in that calendar year (UTC). Previously the parameter was silently dropped — code that passed it expecting a year-scoped response would have received the all-time aggregation.
2026-04-25
Highlights: enforcement responses now return one row per case (with a sources[] array) instead of one row per PDF citation; the error envelope is unified across 401, 403, and 429; three error codes were renamed.
Added
/v1/permitsaccepts Gridify filter expressions. Pass field-level filters via?filter=instead of stacking ad-hoc query params. See filtering for grammar and examples.- OSHA accident investigations. Responses from the OSHA endpoint now include accident records ingested from the DOL v4 API alongside the existing inspection and violation history.
Changed
- Enforcement endpoints return one row per case. Affects
/v1/enforcement,/v1/enforcement/recent,/v1/enforcement/by-license/{licenseNumber}, and/v1/enforcement/trends. Each row now consolidates citations of the same case across multiple PDFs and exposes asources[]array listing every PDF that cited it. Aggregations are no longer inflated by cross-PDF duplicates — consumers computing totals should expect lower counts. - Enforcement
orderTypenormalized to display strings. Responses carry the human-readable order type (e.g.,"Revocation") rather than the legacy enum value. /v1/permits/pointscapped at 100,000 most recent points and now returns the full point shape./v1/viewport-contractorsalso returns the full point shape./v1/permitsdefault ordering changed. Confirm against the API reference if your code depends on result order.
Security
-
Unified error envelope on 401, 403, and 429. Authentication and rate-limit responses now carry the same envelope as 4xx validation errors —
error.code,error.message,error.doc_url,error.request_id, plus theX-Error-Coderesponse header. See errors. -
Error codes renamed. Switch on the new codes:
unauthorized→auth.missing(401, header absent) orauth.invalid(403, header rejected)rate_limit_exceeded→rate_limit.exceeded(429)
Codes are part of the API contract — the rename is a breaking change for anyone branching on the old strings.
-
Stricter 400 envelope on
/v1/*.[BindRequired]failures surface with the correct field-level code; unknown or malformed query parameters are rejected viaquery.unknown_paraminstead of being silently dropped.