Webhooks
3DAPI sends a signed POST to your endpoint when something changes, so you never have to poll. Every event also stays in the events log for 30 days.
Delivery
Every delivery is a POST with a JSON event and three headers: X-Dapi-Webhook-Signature: sha256=<hex>, an HMAC-SHA256 of <timestamp>.<raw body> with your endpoint secret; X-Dapi-Webhook-Timestamp, the Unix time in seconds it was signed (every attempt is signed again; reject a timestamp more than 5 minutes from your clock to stop replays); and X-Dapi-Event-Id, the event id. Answer with any 2xx within 10 seconds. Failed deliveries are retried with backoff for 24 hours, so the same event can arrive more than once: deduplicate on the event id.
POST https://your-server.example/dapi-webhooks
Content-Type: application/json
X-Dapi-Webhook-Signature: sha256=<64 hex characters>
X-Dapi-Webhook-Timestamp: 1791974520
X-Dapi-Event-Id: evt_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu
{
"id": "evt_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
"object": "event",
"type": "order.created",
"created_at": "2026-10-14T10:02:00Z",
"data": {
"object": {
"id": "ord_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
"object": "order",
"number": "PV-SCN-1701A372",
"channel": "storefront",
"status": "paid",
"customer": {
"name": "Ana Popescu",
"email": "ana@example.com",
"phone": null,
"company": null
},
"shipping_address": {
"formatted": "Str. Test 1, Cluj, RO",
"city": "Cluj",
"country": "Romania",
"country_code": "RO"
},
"charges": {
"paid_by": "buyer",
"items": {
"amount": 1800,
"currency": "usd"
},
"shipping": {
"amount": 600,
"currency": "usd"
},
"vat": {
"amount": {
"amount": 0,
"currency": "usd"
},
"rate": 0,
"included": false,
"country": null
},
"sample_credit": null,
"total": {
"amount": 2400,
"currency": "usd"
}
},
"earnings": {
"manufacturing": {
"amount": 900,
"currency": "usd"
},
"platform_fee": {
"amount": 300,
"currency": "usd"
},
"payment_processing": {
"amount": 102,
"currency": "usd"
},
"refunded": {
"amount": 0,
"currency": "usd"
},
"net": {
"amount": 498,
"currency": "usd"
}
},
"shipping": {
"tier": "small",
"carrier": null,
"tracking_number": null,
"shipped_at": null
},
"external": null,
"customer_notes": null,
"qc": {
"open_tickets": 1,
"blocks_shipping": true
},
"created_at": "2026-10-09T08:39:29Z",
"updated_at": "2026-10-09T08:40:02Z",
"item_count": 1,
"items": [
{
"id": "itm_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
"object": "order_item",
"product": {
"id": "prd_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
"object": "product",
"name": "Clicker"
},
"variant": {
"id": "var_bfP0GSqKoZFrD72H5Os0rc7HV9XP6eQJu",
"object": "variant",
"name": "Clicker Blue"
},
"sku": "CLICK-BLUE",
"quantity": 1,
"unit_price": {
"amount": 1800,
"currency": "usd"
},
"total": {
"amount": 1800,
"currency": "usd"
},
"stock_used": [],
"fulfilled_from_stock": false
}
],
"refunds": []
},
"previous": null,
"region": null
}
}Verify signatures
Compute the HMAC over the timestamp and the raw body exactly as received, compare it in constant time, and refuse old timestamps. Answer quickly with a 2xx and do the work afterwards.
import crypto from 'node:crypto';
// Call with the raw request body, before any JSON parsing.
export function verifyDapiWebhook(rawBody, headers, secret) {
const timestamp = headers['x-dapi-webhook-timestamp'];
const signature = headers['x-dapi-webhook-signature'] ?? '';
// Reject old deliveries so a captured request cannot be replayed.
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!timestamp || age > 300) return false;
const expected =
'sha256=' +
crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Event types
order.createdAn order was placed, from any channel. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.
order.paidThe order was paid. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.
order.assignedA production partner took the order. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.
order.in_productionProduction started. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.
order.shippedThe order shipped;
shippinghas the tracking details. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.order.completedThe order was delivered and settled. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.
order.on_holdThere is not enough stock to fulfil the order. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.
order.rejectedThe production partner refused the order. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.
order.returnedThe order was sent back after shipping. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.
order.cancelledThe order was cancelled. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.
qc_ticket.needs_your_actionA quality check has photos waiting for your review, or a production partner asked you a question.
qc_ticket.resolvedA QC ticket was approved, closed or cancelled.
product.approved3DAPI approved every design and extra in a product.
product.rejected3DAPI rejected a design or extra in a product;
review.reasonsays why.stock_batch.purchasedA stock batch was bought and queued for production, by you or by auto-reorder;
originsays which. One checkout buying several batches sends one event per batch, sharingpurchase_reference.stock_batch.completedEvery unit of a stock batch has been made.
stock.lowA region reached the restock threshold you set (
restock[].when_available_below, checked when an order draws units).data.regionnames the region.
Endpoints and the events log
Endpoints are created and edited in the dashboard. The events log keeps the last 30 days of events, delivered or not, so you can catch up after downtime.