Skip to content

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 headers Ducto-Signature, Ducto-Event-Id and Ducto-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
Webhooks | Ducto