API reference
/oauth
POST /oauth/token
OAuth 2.0 client credentials: exchange a client id and secret for a short-lived bearer token
createToken200
Request
TokenRequest {
grant_type: "client_credentials";
client_id: string;
client_secret: string;
scope?: string;
}Response
TokenResponse {
access_token: string;
token_type: "Bearer";
expires_in: number;
scope: string;
}/markets
GET /markets
Where Ducto takes orders, and for which product lines
listMarketsscope markets:read200
Response
MarketsResponse {
markets: Array<{
market: string;
state: string;
zips: Array<string>;
categories: Array<string>;
}>;
}/quotes
POST /quotes
Size a home for a product line and price it, with installer availability
createQuotescope quotes:writeIdempotency-Key201
Request
QuoteRequest {
zip: string;
category?: "mini_split" | "hpwh" | "ev_charger" | "panel";
home: Record<string, unknown>;
tier?: "diy" | "hybrid" | "turnkey";
grade?: "value" | "mid" | "premium";
}Response
QuoteResponse {
quoteId: string;
mode: "live" | "sandbox" | null;
category: string;
market: string;
tier: "diy" | "hybrid" | "turnkey";
grade: "value" | "mid" | "premium";
system: string | null;
sizingNotes: Array<string>;
tiers: Array<{
tier: "diy" | "hybrid" | "turnkey";
eligible: boolean;
blockers: Array<string>;
}>;
equipment: {
available: boolean;
problems: Array<string>;
units: Array<{
brand: string;
series: string;
name: string;
warrantyYears: number | null;
}>;
};
permits: {
lines: Array<{
kind: "mechanical" | "electrical" | "plumbing" | "building";
pulledBy: "homeowner" | "contractor";
feeCents: number;
}>;
basis: string;
};
installers: Array<{
name: string;
rating: number | null;
reviewCount: number;
earliestInstallDate: string;
laborCents?: number;
totalCents: number;
chargedAtCheckoutCents?: number;
}>;
priceNote?: string;
url: string | null;
validDays: number;
}/compliance
POST /compliance/checks
What a set of products needs at a ZIP: permits and who pulls them, credentials, and one panel check
checkCompliancescope compliance:readIdempotency-Key200
Request
ComplianceCheckRequest {
zip: string;
items: Array<{
category: "mini_split" | "hpwh" | "ev_charger" | "panel";
home: Record<string, unknown>;
}>;
electrical?: HomeElectrical;
}Response
ComplianceCheckResponse {
mode: "live" | "sandbox" | null;
market: {
name: string;
state: string;
feesVerified: boolean;
};
items: Array<{
category: string;
role: "anchor" | "addon";
tier: "diy" | "hybrid" | "turnkey";
eligible: boolean;
blockers: Array<string>;
permits: {
lines: Array<{
kind: string;
pulledBy: string;
feeCents: number;
}>;
basis: string;
office: string | null;
};
credentials: Array<Array<string>>;
notes: Array<string>;
}>;
panel: PanelCheck;
}POST /compliance/plans
The compliance plan for an exact address: the permit office by boundary, permits, who pulls them, fees, inspections, licenses, siting and documents, each line citing its rule and source
createCompliancePlanscope compliance:readIdempotency-Key201
Request
CompliancePlanRequest {
address: {
street: string;
city: string;
state: string;
zip: string;
};
items: Array<{
category: "mini_split" | "hpwh" | "ev_charger" | "panel";
home: Record<string, unknown>;
}>;
}Response
CompliancePlanResponse {
id: string;
version: number;
mode: "live" | "sandbox" | null;
address: AddressResolution;
office: {
id: string;
name: string;
} | null;
basis: "boundary" | "zip" | "none";
plan: ComplianceDecision;
createdAt: string;
}GET /compliance/plans/{id}
A compliance plan you requested
getCompliancePlanscope compliance:read200
Response
CompliancePlanResponse {
id: string;
version: number;
mode: "live" | "sandbox" | null;
address: AddressResolution;
office: {
id: string;
name: string;
} | null;
basis: "boundary" | "zip" | "none";
plan: ComplianceDecision;
createdAt: string;
}/homes
GET /homes/{id}
A home's record, where its owner granted you access: limited to the grant's scopes, and every read is shown to the owner
getHomescope homes:read200
Response
HomeRecord {
id: string;
generatedAt: string;
scopes: Array<"home:read" | "systems:read" | "readings:read" | "permits:read" | "warranty:read" | "service:read" | "photos:read">;
home?: {
address: {
street: string;
city: string;
state: string;
zip: string;
};
placeGeoid: string | null;
countyGeoid: string | null;
permitOffice: string | null;
climateZone: number | null;
parcel: {
id: string;
source: string | null;
} | null;
previousOwners: number;
};
systems: Array<HomeSystem>;
}/projects
POST /projects
Price a project (a heat pump or water heater and its add-ons) for a homeowner to finish on Ducto
createProjectscope projects:writeIdempotency-Key201
Request
ProjectRequest {
zip: string;
anchor: {
category: "mini_split" | "hpwh" | "ev_charger" | "panel";
home: Record<string, unknown>;
tier?: "diy" | "hybrid" | "turnkey";
grade?: "value" | "mid" | "premium";
};
addons?: Array<{
category: "mini_split" | "hpwh" | "ev_charger" | "panel";
home: Record<string, unknown>;
tier?: "diy" | "hybrid" | "turnkey";
grade?: "value" | "mid" | "premium";
}>;
electrical?: Record<string, unknown>;
merchant?: "ducto" | "retailer";
cart?: Array<CartLine>;
}Response
ProjectResponse {
id: string;
mode: "live" | "sandbox";
status: "quoted" | "ordered" | "expired";
market: string;
items: Array<{
category: string;
role: "anchor" | "addon";
system: string;
installer: string;
installDate: string;
totals: {
chargeNowCents: number;
chargeOnConfirmCents: number;
totalCents: number;
};
}>;
panel: {
status: "ok" | "review" | "over";
needsPanelWork: boolean;
notes: Array<string>;
};
totals: {
chargeNowCents: number;
chargeOnConfirmCents: number;
totalCents: number;
};
url: string | null;
expiresAt: string;
merchant?: "ducto" | "retailer";
cart?: {
fits: boolean;
equipmentCents: number;
unused: Array<string>;
};
}GET /projects/{id}
A project you priced
getProjectscope projects:read200
Response
ProjectResponse {
id: string;
mode: "live" | "sandbox";
status: "quoted" | "ordered" | "expired";
market: string;
items: Array<{
category: string;
role: "anchor" | "addon";
system: string;
installer: string;
installDate: string;
totals: {
chargeNowCents: number;
chargeOnConfirmCents: number;
totalCents: number;
};
}>;
panel: {
status: "ok" | "review" | "over";
needsPanelWork: boolean;
notes: Array<string>;
};
totals: {
chargeNowCents: number;
chargeOnConfirmCents: number;
totalCents: number;
};
url: string | null;
expiresAt: string;
merchant?: "ducto" | "retailer";
cart?: {
fits: boolean;
equipmentCents: number;
unused: Array<string>;
};
}POST /projects/{id}/bookings
Book a project priced with merchant "retailer": a link for the homeowner to accept the installation contract
bookProjectscope projects:writeIdempotency-Key201
Request
BookingRequest {
homeowner: {
name: string;
email: string;
phone?: string;
};
address: {
street: string;
city: string;
};
retailerOrderRef: string;
}Response
Booking {
id: string;
mode: "live" | "sandbox";
status: "awaiting_homeowner" | "accepted" | "cancelled" | "expired";
retailerOrderRef: string;
url: string | null;
projectRef: string | null;
orders: Array<{
ref: string;
status: string;
category: string;
}>;
cancellation: Array<string>;
expiresAt: string;
}/catalog
GET /catalog/products
The products Ducto installs, to map your SKUs to
listProductsscope catalog:read200
Response
ProductList {
products: Array<{
sku: string;
brand: string;
series: string;
model: string;
name: string;
category: string;
line: string;
nominalBtu: number | null;
ports: number | null;
headStyle: string | null;
coldClimate: boolean;
}>;
}PUT /catalog/sku-mappings
Map your own SKUs to Ducto products (retailers; used in carts and eligibility)
mapSkusscope projects:write200
Request
SkuMappings {
mappings: Array<{
sku: string;
productSku: string;
}>;
}Response
SkuMappingResult {
mapped: number;
unknownProducts: Array<string>;
}PUT /catalog/inventory
Stock and prices from a supplier (applied at once when clean; sandbox: matched, never applied)
updateInventoryscope inventory:write200
Request
InventoryUpdate {
rows: Array<{
supplierSku: string;
costCents: number;
qty: number;
leadDays?: number | null;
mapCents?: number | null;
brand?: string | null;
model?: string | null;
name?: string | null;
category?: string | null;
nominalBtu?: number | null;
imageUrl?: string | null;
specSheetUrl?: string | null;
}>;
fullFeed?: boolean;
label?: string;
}Response
InventoryResult {
importId: string | null;
mode: "live" | "sandbox" | null;
applied: boolean;
summary: {
rows: number;
matched: number;
unmatched: number;
ignored: number;
priceChanges: number;
bigChanges: number;
};
unmatchedSkus: Array<string>;
}/widget-sessions
POST /widget-sessions
A short-lived session for a drop-in widget (sizing, install-and-permit, booking): its token goes to the browser instead of an API key
createWidgetSessionscope quotes:writeIdempotency-Key201
Request
WidgetSessionRequest {
kind: "sizing" | "install" | "booking";
zip?: string;
merchant?: "ducto" | "retailer";
cart?: Array<CartLine>;
projectId?: string;
retailerOrderRef?: string;
allowedOrigins?: Array<string>;
theme?: WidgetTheme;
}Response
WidgetSessionResponse {
id: string;
kind: "sizing" | "install" | "booking";
mode: "live" | "sandbox";
token: string;
url: string;
expiresAt: string;
}/sandbox
POST /sandbox/bookings/{id}/accept
Sandbox only: the scripted homeowner accepts the booking
sandboxAcceptBookingscope projects:writeIdempotency-Key200
Response
Booking {
id: string;
mode: "live" | "sandbox";
status: "awaiting_homeowner" | "accepted" | "cancelled" | "expired";
retailerOrderRef: string;
url: string | null;
projectRef: string | null;
orders: Array<{
ref: string;
status: string;
category: string;
}>;
cancellation: Array<string>;
expiresAt: string;
}POST /sandbox/orders/{ref}/advance
Sandbox only: time travel. The scripted installer and homeowner take the order as far as asked, stopping where it waits on you
advanceSandboxOrderscope orders:writeIdempotency-Key200
Request
AdvanceRequest {
to?: "confirmed" | "in_progress" | "completed";
}Response
AdvanceResult {
ref: string;
status: string;
waitingOn: string | null;
steps: Array<string>;
}/eligibility
POST /eligibility
Whether your SKUs can be installed at a ZIP, and what the install needs
checkEligibilityscope quotes:writeIdempotency-Key200
Request
EligibilityRequest {
zip: string;
items: Array<{
sku: string;
qty: number;
priceCents?: number;
}>;
}Response
EligibilityResponse {
zip: string;
served: boolean;
market: string | null;
items: Array<{
sku: string;
product: string | null;
line: string | null;
installable: boolean;
reason: string | null;
}>;
installers: number;
needs: Array<string>;
}/bookings
GET /bookings/{id}
A booking and its orders
getBookingscope projects:read200
Response
Booking {
id: string;
mode: "live" | "sandbox";
status: "awaiting_homeowner" | "accepted" | "cancelled" | "expired";
retailerOrderRef: string;
url: string | null;
projectRef: string | null;
orders: Array<{
ref: string;
status: string;
category: string;
}>;
cancellation: Array<string>;
expiresAt: string;
}POST /bookings/{id}/cancel
Cancel a booking the homeowner hasn't accepted
cancelBookingscope projects:writeIdempotency-Key200
Response
Booking {
id: string;
mode: "live" | "sandbox";
status: "awaiting_homeowner" | "accepted" | "cancelled" | "expired";
retailerOrderRef: string;
url: string | null;
projectRef: string | null;
orders: Array<{
ref: string;
status: string;
category: string;
}>;
cancellation: Array<string>;
expiresAt: string;
}/orders
POST /orders/{ref}/shipments
You shipped the equipment for an order you sold: the units' models and serials (checked against the order)
reportShipmentscope orders:writeIdempotency-Key200
Request
ShipmentRequest {
carrier: string;
tracking: string;
units: Array<{
model: string;
serial: string;
}>;
}Response
ShipmentResult {
ref: string;
status: "shipped" | "delivered" | "problem";
verified: boolean;
problems: Array<string>;
}POST /orders/{ref}/delivery
The equipment was delivered (serials checked against the shipment before work starts)
reportDeliveryscope orders:writeIdempotency-Key200
Request
DeliveryRequest {
serials?: Array<string>;
}Response
ShipmentResult {
ref: string;
status: "shipped" | "delivered" | "problem";
verified: boolean;
problems: Array<string>;
}POST /orders/{ref}/cancel
Cancel an order you booked, by the published cancellation rules
cancelOrderscope orders:writeIdempotency-Key200
Request
CancelRequest {
reason: string;
}Response
CancelResult {
ref: string;
outcome: "cancelled" | "requested";
rules: Array<string>;
}GET /orders/{ref}
The status of an order attributed to you
getOrderscope orders:read200
Response
OrderStatus {
ref: string;
project: string | null;
role: "anchor" | "addon" | null;
category: string;
status: string;
statusLabel: string;
system: string;
installDate: string | null;
}/supply
PUT /supply/inventory
A supplier's stock at one of its locations, by SKU (stock older than 72 hours isn't quoted)
reportStockscope inventory:write200
Request
StockReport {
location: string;
items: Array<{
sku: string;
qty: number;
}>;
reportedAt?: string;
}Response
StockResult {
mode: "live" | "sandbox" | null;
location: string;
applied: boolean;
matched: number;
unmatchedSkus: Array<string>;
}PUT /supply/prices
A supplier's prices to Ducto by SKU and tier (list, contract, promotional) with their dates
reportPricesscope inventory:write200
Request
PriceList {
items: Array<{
sku: string;
tier: "list" | "contract" | "promo";
costCents: number;
effectiveFrom: string;
effectiveTo?: string | null;
}>;
}Response
PriceResult {
mode: "live" | "sandbox" | null;
applied: boolean;
matched: number;
unmatchedSkus: Array<string>;
}POST /supply/edi
An X12 document from a supplier: 846 inventory advice (and purchase-order replies)
receiveEdiscope inventory:writeIdempotency-Key200
Request
EdiDocument {
document: string;
}Response
EdiResult {
type: string;
control: string;
applied: boolean;
detail: Record<string, unknown>;
}GET /supply/purchase-orders
A supplier's open purchase orders, each also as an X12 850 (reply with 855, 856 and 810 to /supply/edi)
listPurchaseOrdersscope purchase_orders:read200
Response
PurchaseOrders {
orders: Array<{
po: string;
status: string;
billing: string;
fulfillment: "ship" | "pickup";
shipTo: {
name: string;
street: string;
city: string;
state: string;
zip: string;
};
lines: Array<{
sku: string;
qty: number;
unitCents: number;
}>;
x12: string;
}>;
}/manufacturer
PUT /manufacturer/products
A manufacturer's product data and extended-performance tables, for its verified brands
updateBrandProductsscope brand:write200
Request
ProductData {
brand?: string;
items: Array<{
model: string;
seer2?: number;
hspf2?: number;
heatingBtuAt5F?: number;
minOutdoorTempF?: number;
refrigerant?: string;
warrantyYears?: number;
performance?: Array<{
mode: "heating" | "cooling";
outdoorF: number;
capacityBtu: number;
inputWatts: number;
}>;
}>;
}Response
ProductDataResult {
brand: string;
updated: number;
unmatchedModels: Array<string>;
}PUT /manufacturer/certified-installers
A manufacturer's certified-installer list for a brand (installers' certifications are checked against it)
updateCertifiedInstallersscope brand:write200
Request
CertifiedInstallers {
brand?: string;
installers: Array<{
number: string;
name: string;
tier?: string;
expiresOn?: string;
}>;
}Response
CertifiedInstallersResult {
brand: string;
listed: number;
credentialsChecked: number;
}GET /manufacturer/warranty-registrations
Units of your brands on finished installs, with the owner only where the homeowner asked Ducto to register it
listWarrantyRegistrationsscope brand:read200
Response
WarrantyRegistrations {
registrations: Array<{
brand: string;
model: string;
serial: string;
unit: string;
installedOn: string;
installer: string;
installerLicense: string;
state: string;
order: string;
owner: {
name: string;
email: string;
phone: string;
street: string;
city: string;
zip: string;
} | null;
}>;
}GET /manufacturer/report
The brand performance report for your brands (figures for fewer than five jobs are hidden)
getBrandReportscope brand:read200
Response
BrandReports {
reports: Array<Record<string, unknown>>;
}Errors on every endpoint are RFC 9457 problems: { type, title, status, detail, code, instance, errors? }, where code is one of invalid_request, unauthorized, forbidden, not_found, conflict, idempotency_key_reused, idempotency_key_required, not_served, category_not_offered, unavailable, rate_limited and internal.