Webhooks
Ducto posts events to the endpoints you register on your dashboard, per mode (sandbox endpoints get sandbox events).
Delivery
- A POST with a JSON body
{ id, type, created, data }and headersDucto-Signature,Ducto-Event-IdandDucto-Event-Type. - Answer with any 2xx within 10 seconds. Anything else is retried after 1 minute, 5, 30, then 2, 6, 12 and 24 hours; after that the event waits for Ducto to replay it, and you can replay any delivery from your dashboard.
- Events can arrive more than once and out of order: de-duplicate on
id, and read the current state with the API when order matters. - Nothing about the homeowner is in a webhook.
Verifying the signature
Ducto-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body" with your endpoint's secret>. Use the raw body, and reject timestamps more than five minutes off.
With the SDK
import { verifyWebhook } from "@ducto/sdk";
// The raw body, exactly as received (not re-serialized JSON).
const ok = await verifyWebhook(process.env.DUCTO_WEBHOOK_SECRET!, rawBody, req.headers.get("ducto-signature"));
if (!ok) return new Response(null, { status: 400 });
const event = JSON.parse(rawBody); // { id, type, created, data }
// Deliveries can repeat: skip an event.id you've already handled.Node, no SDK
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, rawBody, header, toleranceSec = 300) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > toleranceSec) return false;
const mac = createHmac("sha256", secret).update(`${m[1]}.${rawBody}`).digest();
return timingSafeEqual(mac, Buffer.from(m[2], "hex"));
}Python
import hashlib, hmac, re, time
def verify(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "")
if not m or abs(time.time() - int(m.group(1))) > tolerance:
return False
mac = hmac.new(secret.encode(), m.group(1).encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, m.group(2))Events
order.status_changed
An order you booked, or one attributed to you, changes status.
- ref
- Ducto's order reference
- status
- the new status
- category
- the product line
- projectRole
- anchor or addon
- installDate
- YYYY-MM-DD or null
- retailerOrderRef
- your order reference (orders you booked)
booking.accepted
The homeowner accepted the installation contract for your booking.
- booking
- the booking id
- retailerOrderRef
- your order reference
- projectRef
- Ducto's project reference
- orders
- the orders in the project
order.ready_to_ship
The installer confirmed an order you sold: ship the equipment.
- ref
- the order
- retailerOrderRef
- your order reference
- po
- the purchase order number
- installDate
- YYYY-MM-DD
- shipTo
- street, city, state, zip
- lines
- Ducto SKUs and quantities
delivery.verified
The delivery you reported matched the shipment; work can start.
- ref
- the order
- retailerOrderRef
- your order reference
- tracking
- the tracking number
delivery.problem
The serials delivered didn't match the shipment; work waits until it's sorted out.
- ref
- the order
- retailerOrderRef
- your order reference
- tracking
- the tracking number
- problems
- what didn't match