Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.1.0
info:
title: BuilderVet API
version: "0.1.0"
description: >
Dated NSW and QLD licence, company and regulatory evidence. Consult per-source coverage
before interpreting an absent finding. No safety rating, phoenix detection,
solvency prediction or complete DA coverage. Public evidence is free; paid
value is monitoring, private work and exports. Legacy fields stay compatible.
x-collection-policy: Logged-out, public, robots-honoured, entity-not-personal by design.
servers:
# Live production origin. The .com.au domain is owned by a third party —
# do not reference it until we own a real domain (product is being renamed).
- url: https://api.buildervet.au
paths:
/v1/coverage/{slug}:
get:
operationId: getBuilderCoverage
summary: Per-source check state and timestamps; never inferred from entity update time.
parameters: [{name: slug, in: path, required: true, schema: {type: string}}]
responses:
"200": {description: "coverage entries with state finding, checked, recorded (dated value on file, no live check yet), not_checked, stale, or unavailable; lastAttemptAt, lastSuccessAt, nextDueAt, detail, sourceUrl"}
"429": {description: Rate limit; Retry-After specifies seconds}
/v1/check/{slug}:
post:
operationId: startBuilderCheck
summary: Run a live check of the NSW licence, ASIC and three NSW registers for one builder. Returns at once; poll coverage until lastAttemptAt is at or after startedAt. Current sources are not re-fetched.
parameters: [{name: slug, in: path, required: true, schema: {type: string}}]
responses:
"202": {description: "started; body has startedAt and poll path"}
"404": {description: Unknown builder}
"422": {description: Outside NSW coverage}
"429": {description: Rate limit (6 per minute per client)}
"503": {description: Check service unavailable or hourly source cap reached}
/v1/hbc-site:
post:
operationId: hbcForSite
summary: HBC certificates on record for one NSW site address (street number, street and postcode). Covers certificates from licences BuilderVet has read; not proof that none exists.
requestBody: {required: true, content: {application/json: {schema: {type: object, properties: {address: {type: string}}}}}}
responses:
"200": {description: "checked, detail, certificates (certificateNo, siteAddress, issuedOn, licensee, contractorLicence, slug), coverage counts"}
"400": {description: Body is not JSON}
/v1/quote-match:
post:
operationId: matchQuote
summary: Check that the name, ABN or ACN, licence and site address from a quote point to one builder, and whether an HBC certificate for the site is on record. Nothing is stored.
requestBody: {required: true, content: {application/json: {schema: {type: object, properties: {name: {type: string}, abn: {type: string}, acn: {type: string}, licence: {type: string}, address: {type: string}}}}}}
responses:
"200": {description: "verdict (match, mismatch, not_found, name_only, incomplete), builder, identifiers with detail, hbc site result, limits"}
"400": {description: Body is not JSON}
"429": {description: Rate limit}
/v1/rewards:
get:
operationId: getMemberRewards
summary: Private watch-credit wallet, own invitation labels/status and bounded history; no cash balance or friend identity.
security: [{bearerSession: []}]
responses:
"200": {description: "policy, wallet trial/annual counts (ready, pending, used, expired, reversed), confirmed paidFriends progress, latest 100 invitations and credits"}
"401": {description: Sign in required}
"403": {description: Email verification required}
"503": {description: Referral programme disabled}
/v1/rewards/invite:
get:
operationId: checkMemberInvitation
summary: Public validity and expiry only; no inviter identity or private label.
parameters: [{name: token, in: query, required: true, schema: {type: string, pattern: '^[a-f0-9]{32}$'}}]
responses:
"200": {description: "valid boolean and optional expiresAt; used/expired/ineligible links return valid false"}
"503": {description: Referral programme disabled}
/v1/rewards/invites:
post:
operationId: createMemberInvitation
summary: Create a private single-person invitation; does not send email or messages. Obtain member intent before creating or sharing.
security: [{bearerSession: []}]
requestBody: {required: true, content: {application/json: {schema: {type: object, properties: {label: {type: string, maxLength: 80}}}}}}
responses:
"200": {description: "invite with id, private label, URL, createdAt, expiresAt, status"}
"400": {description: Invalid label or body}
"401": {description: Sign in required}
"403": {description: Email verification required}
"409": {description: Daily or lifetime invite cap reached}
"503": {description: Referral programme disabled}
/v1/rewards/redeem:
post:
operationId: redeemMemberWatchCredit
summary: Spend one owned ready pass on one exact NSW builder. Requires explicit member choice; no swaps. Same credit and builder retries are idempotent.
security: [{bearerSession: []}]
requestBody: {required: true, content: {application/json: {schema: {type: object, required: [creditId, entitySlug], properties: {creditId: {type: string}, entitySlug: {type: string}}}}}}
responses:
"200": {description: "ok, entitySlug, expiresAt; existing workspace API provisions the saved baseline and watch, with server repair"}
"400": {description: Invalid credit or builder}
"401": {description: Sign in required}
"403": {description: Email verification required}
"404": {description: Credit or builder not found}
"409": {description: "Pending, expired, reversed, already-used credit; active equal-or-longer watch; or checkout in progress"}
"422": {description: Builder outside NSW}
"503": {description: Referral programme disabled}
/v1/workspace:
get:
operationId: getWorkspace
summary: Private account folders, watches and notifications; no-store.
security: [{bearerSession: []}]
responses:
"200": {description: Account-owned workspace}
"401": {description: Sign in required}
/v1/billing:
get:
operationId: getBilling
summary: Paid-through rights, legacy rights and checkout availability.
security: [{bearerSession: []}]
responses:
"200": {description: Billing summary}
"401": {description: Sign in required}
/v1/billing/checkout:
post:
operationId: createMonitoringCheckout
summary: Verified account checkout; total AUD99 homeowner or AUD199 monthly professional.
security: [{bearerSession: []}]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [product]
properties:
product: {type: string, enum: [homeowner, professional]}
entitySlug: {type: string, description: Required for homeowner}
responses:
"200": {description: Stripe hosted checkout URL}
"403": {description: Email proof required}
"409": {description: Existing access or confirming payment; do not pay again}
"503": {description: Paid release not enabled}
/v1/billing/portal:
post:
operationId: openBillingPortal
summary: Account-owned Customer Portal; invoices, billing details (business name, ABN, address), cancel professional renewal at period end.
security: [{bearerSession: []}]
responses:
"200": {description: Stripe portal URL}
"401": {description: Sign in required}
/v1/billing/invoices:
get:
operationId: listInvoices
summary: The account's Stripe invoices (latest 100, no drafts), reports bought, and a summary by Sydney calendar month. Stripe is the record; legacy purchases show without an invoice.
security: [{bearerSession: []}]
responses:
"200": {description: "{customer, invoices, reports, months, hasMore, invoicesError, portal, emailTo}"}
"401": {description: Sign in required}
/v1/billing/invoices/{invoiceId}/email:
post:
operationId: emailInvoice
summary: Email a copy of one of the account's invoices to the sign-in address. Three a day per invoice.
security: [{bearerSession: []}]
parameters:
- {name: invoiceId, in: path, required: true, schema: {type: string, pattern: "^in_[A-Za-z0-9]+$"}}
responses:
"200": {description: "{sent: true, to}"}
"401": {description: Sign in required}
"404": {description: No such invoice on this account}
"429": {description: Daily limit for this invoice reached}
"502": {description: Email provider refused; try again}
/v1/suggest:
get:
operationId: suggestBuilders
summary: Typeahead autocomplete — top 8 matches for a partial name, licence number, ABN or ACN. Edge-cached and fast enough to call per keystroke.
parameters:
- { name: q, in: query, required: true, schema: { type: string, minLength: 2, maxLength: 80 } }
responses:
"200": { description: Suggestions (empty results for queries under 2 chars), content: { application/json: { schema: { $ref: "#/components/schemas/SearchResults" } } } }
/v1/search:
get:
operationId: searchBuilders
summary: Search builders by name (optionally filtered by postcode).
parameters:
- { name: q, in: query, required: true, schema: { type: string, minLength: 2 } }
- { name: postcode, in: query, required: false, schema: { type: string } }
responses:
"200": { description: Matches, content: { application/json: { schema: { $ref: "#/components/schemas/SearchResults" } } } }
"400": { description: Query shorter than 2 characters }
/v1/builder/{slug}:
get:
operationId: getBuilder
summary: Full risk profile for one builder.
parameters: [ { name: slug, in: path, required: true, schema: { type: string } } ]
responses:
"200": { description: Profile, content: { application/json: { schema: { $ref: "#/components/schemas/BuilderProfile" } } } }
"404": { description: Not found }
/v1/builder/{slug}/timeline:
get:
operationId: getBuilderTimeline
summary: Dated event timeline (licence changes, insolvency, disciplinary, etc).
parameters: [ { name: slug, in: path, required: true, schema: { type: string } } ]
responses:
"200": { description: Timeline, content: { application/json: { schema: { $ref: "#/components/schemas/Timeline" } } } }
/v1/export:
get:
operationId: exportAll
summary: Bulk export of every builder profile with licences, risk signals and events (used to build the static site; large response).
responses:
"200":
description: All builders
content:
application/json:
schema:
type: object
properties:
count: { type: integer }
builders: { type: array, items: { $ref: "#/components/schemas/BuilderProfile" } }
/v1/waitlist:
post:
operationId: joinWaitlist
summary: Record explicit launch interest in a homeowner report or professional monitoring offer.
requestBody:
required: true
content: { application/json: { schema: { type: object, required: [email, intent, offerCode, consent], properties: { email: { type: string, format: email, maxLength: 254 }, intent: { type: string, enum: [report, monitor] }, offerCode: { type: string, enum: [homeowner, professional] }, consent: { type: boolean, enum: [true] }, entitySlug: { type: string, pattern: "^[a-z0-9-]{1,80}$" }, ts: { type: string, description: "Form-render timestamp for bot filtering; not used as the stored consent timestamp." }, website: { type: string, description: "Leave blank; honeypot." }, session: { type: string } } } } }
responses:
"200": { description: "Check saved=true before showing success; filtered traffic returns saved=false.", content: { application/json: { schema: { type: object, required: [ok, saved], properties: { ok: { type: boolean }, saved: { type: boolean } } } } } }
"400": { description: Invalid email, intent/offer pair, consent or entitySlug }
/v1/referral:
get:
operationId: referralStatus
summary: Referral-ladder progress for a code (count only, never emails).
parameters:
- { name: code, in: query, required: true, schema: { type: string, pattern: "^[a-z0-9]{8}$" } }
responses:
"200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ReferralStatus" } } } }
"400": { description: Invalid code }
/v1/watch:
post:
operationId: watchAddress
summary: Watch an address for register events (free alerts; explicit consent required).
requestBody:
required: true
content: { application/json: { schema: { type: object, required: [email, address, consent], properties: { email: { type: string, format: email, maxLength: 254 }, address: { type: string, minLength: 5, maxLength: 160 }, postcode: { type: string, pattern: "^\\d{4}$" }, builderName: { type: string, maxLength: 120 }, consent: { type: boolean }, ref: { type: string, pattern: "^[a-z0-9]{8}$" } } } } }
responses:
"200": { description: "OK; returns the email's referral code and referral count.", content: { application/json: { schema: { $ref: "#/components/schemas/ReferralStatus" } } } }
"400": { description: Invalid email, address or missing consent }
/v1/watch/unsubscribe:
get:
operationId: unsubscribeWatch
summary: One-click unsubscribe from alert emails (token from the email footer).
parameters:
- { name: token, in: query, required: true, schema: { type: string, pattern: "^[a-z0-9]{16}$" } }
responses:
"200": { description: "Always OK (HTML confirmation; never confirms token existence)." }
/v1/auth/request-link:
post:
operationId: requestSignInLink
summary: Email a one-time passwordless sign-in link (always returns ok; no account enumeration).
requestBody:
required: true
content: { application/json: { schema: { type: object, required: [email], properties: { email: { type: string, format: email, maxLength: 254 } } } } }
responses:
"200": { description: "Always {ok:true}. The link expires in 15 minutes and works once." }
/v1/auth/verify:
get:
operationId: verifySignInLink
summary: Exchange a magic-link token for a session (browser redirect; token from the sign-in email).
parameters:
- { name: token, in: query, required: true, schema: { type: string, pattern: "^[0-9a-f]{64}$" } }
responses:
"302": { description: "Redirects to the account page with the bearer session token in the URL fragment (or #error=expired)." }
/v1/me:
get:
operationId: getAccount
summary: The signed-in account's profile, address watches, referral progress and purchased reports.
security: [{ bearerSession: [] }]
responses:
"200": { description: "Account view.", content: { application/json: { schema: { $ref: "#/components/schemas/AccountView" } } } }
"401": { description: Missing, expired or revoked session token }
/v1/auth/logout:
post:
operationId: logout
summary: Revoke the current session token (idempotent).
security: [{ bearerSession: [] }]
responses:
"200": { description: "Always {ok:true}." }
components:
securitySchemes:
bearerSession:
type: http
scheme: bearer
description: Session token minted by /v1/auth/verify (64-char hex).
schemas:
AccountView:
type: object
properties:
email: { type: string }
createdAt: { type: string }
watches: { type: array, items: { type: object, properties: { address: { type: string }, postcode: { type: string, nullable: true }, builder: { type: string, nullable: true }, active: { type: integer }, createdAt: { type: string } } } }
refCode: { type: string, nullable: true }
referrals: { type: integer }
purchases: { type: array, items: { type: object, properties: { kind: { type: string }, entitySlug: { type: string, nullable: true }, createdAt: { type: string } } } }
ReferralStatus:
type: object
properties:
ok: { type: boolean }
refCode: { type: string, description: "This email's shareable referral code." }
referrals: { type: integer, description: "Distinct emails referred by this code." }
SearchResults:
type: object
properties:
query: { type: string }
results:
type: array
items:
type: object
properties: { slug: { type: string }, displayName: { type: string }, postcode: { type: string, nullable: true }, status: { type: string, nullable: true } }
Licence:
type: object
properties: { licenceNo: { type: string }, class: { type: string }, status: { type: string }, grantedOn: { type: string }, expiresOn: { type: string }, suburb: { type: string }, postcode: { type: string } }
BuilderProfile:
type: object
properties:
slug: { type: string }
displayName: { type: string }
legalName: { type: string }
abn: { type: string, nullable: true }
acn: { type: string, nullable: true }
asicStatus: { type: string, nullable: true }
licences: { type: array, items: { $ref: "#/components/schemas/Licence" } }
riskSignals: { type: array, items: { type: object, properties: { signal: { type: string }, severity: { type: string } } } }
firstObservedByBuilderCheck: { type: string, description: "When BuilderVet's archive first captured this entity." }
Timeline:
type: object
properties:
slug: { type: string }
events:
type: array
items:
type: object
properties:
kind: { type: string }
severity: { type: string, enum: [info, watch, risk] }
summary: { type: string }
occurredOn: { type: string, nullable: true }
sourceId: { type: string }
onlyAvailableHere: { type: boolean, description: "True when upstream has deleted this record; BuilderVet retained a dated copy. This does not imply exclusive access." }