Webhooks

View as Markdown

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.

A delivery
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
Body of order.created
{
  "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.

Verify a delivery
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.created

    An order was placed, from any channel. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.

  • order.paid

    The order was paid. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.

  • order.assigned

    A production partner took the order. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.

  • order.in_production

    Production started. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.

  • order.shipped

    The order shipped; shipping has the tracking details. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.

  • order.completed

    The order was delivered and settled. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.

  • order.on_hold

    There 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.rejected

    The production partner refused the order. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.

  • order.returned

    The order was sent back after shipping. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.

  • order.cancelled

    The order was cancelled. Designers receive the order as get_order returns it; integrators as get_integrator_order returns it.

  • qc_ticket.needs_your_action

    A quality check has photos waiting for your review, or a production partner asked you a question.

  • qc_ticket.resolved

    A QC ticket was approved, closed or cancelled.

  • product.approved

    3DAPI approved every design and extra in a product.

  • product.rejected

    3DAPI rejected a design or extra in a product; review.reason says why.

  • stock_batch.purchased

    A stock batch was bought and queued for production, by you or by auto-reorder; origin says which. One checkout buying several batches sends one event per batch, sharing purchase_reference.

  • stock_batch.completed

    Every unit of a stock batch has been made.

  • stock.low

    A region reached the restock threshold you set (restock[].when_available_below, checked when an order draws units). data.region names 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.