# Webhooks

For designers and integrators.

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**

```http
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**

```json
{
  "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": [],
          "custom_file": null,
          "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 (Node.js)**

```javascript
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);
}
```

**Verify a delivery (Python)**

```python
import hashlib
import hmac
import time

# Call with the raw request body (bytes), before any JSON parsing.
def verify_dapi_webhook(raw_body, headers, secret):
    timestamp = headers.get("x-dapi-webhook-timestamp")
    signature = headers.get("x-dapi-webhook-signature", "")

    # Reject old deliveries so a captured request cannot be replayed.
    if not timestamp or abs(time.time() - int(timestamp)) > 300:
        return False

    signed = timestamp.encode() + b"." + raw_body
    expected = "sha256=" + hmac.new(
        secret.encode(), signed, hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)
```

## Event types

-   [`order.created`](https://public-api.dapi.digital/docs/reference/events/order.created.md)
    
    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`](https://public-api.dapi.digital/docs/reference/events/order.paid.md)
    
    The order was paid. Designers receive the order as get\_order returns it; integrators as get\_integrator\_order returns it.
    
-   [`order.assigned`](https://public-api.dapi.digital/docs/reference/events/order.assigned.md)
    
    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`](https://public-api.dapi.digital/docs/reference/events/order.in_production.md)
    
    Production started. Designers receive the order as get\_order returns it; integrators as get\_integrator\_order returns it.
    
-   [`order.shipped`](https://public-api.dapi.digital/docs/reference/events/order.shipped.md)
    
    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`](https://public-api.dapi.digital/docs/reference/events/order.completed.md)
    
    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`](https://public-api.dapi.digital/docs/reference/events/order.on_hold.md)
    
    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`](https://public-api.dapi.digital/docs/reference/events/order.rejected.md)
    
    The production partner refused the order. Designers receive the order as get\_order returns it; integrators as get\_integrator\_order returns it.
    
-   [`order.returned`](https://public-api.dapi.digital/docs/reference/events/order.returned.md)
    
    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`](https://public-api.dapi.digital/docs/reference/events/order.cancelled.md)
    
    The order was cancelled. Designers receive the order as get\_order returns it; integrators as get\_integrator\_order returns it.
    
-   [`checkout.expired`](https://public-api.dapi.digital/docs/reference/events/checkout.expired.md)
    
    Integrators only. A `checkout` order was not paid within 24 hours of its link opening, so the order was cancelled. `data.object` is the order as get\_integrator\_order returns it.
    
-   [`quote.ready`](https://public-api.dapi.digital/docs/reference/events/quote.ready.md)
    
    Integrators only. Slicing finished and the quote is priced: `price` is set, for the file that gets printed (`file.production_version` when 3DAPI adapted your upload, otherwise the upload). Sent once per quote. `data.object` is the quote as get\_quote returns it; its download links were signed when the event was sent and expire 15 minutes later, so fetch the quote for fresh ones.
    
-   [`quote.failed`](https://public-api.dapi.digital/docs/reference/events/quote.failed.md)
    
    Integrators only. The file could not be priced: slicing failed, took too long and was given up, or could not be started. `failure_reason` says why; upload a corrected file as a new quote. A failed quote no longer counts toward your upload allowance.
    
-   [`quote.approved`](https://public-api.dapi.digital/docs/reference/events/quote.approved.md)
    
    Integrators only. 3DAPI approved the file in your approval request: `approval.status` is `approved` and the file counts toward your account approval. Sent when 3DAPI completes the review of your approval request, with the final decision on the file, just before the outcome event.
    
-   [`quote.rejected`](https://public-api.dapi.digital/docs/reference/events/quote.rejected.md)
    
    Integrators only. 3DAPI rejected the file in your approval request; `approval.reason` says why. Rejected quotes cannot be submitted again: upload a corrected file as a new quote. Sent when 3DAPI completes the review of your approval request, with the final decision on the file.
    
-   [`approval_request.approved`](https://public-api.dapi.digital/docs/reference/events/approval_request.approved.md)
    
    Integrators only. Your account became approved: quotes are unlimited and you can place orders. Sent once, when a completed review approves the account or when 3DAPI approves it directly. `data.object` is your latest approval request as get\_approval\_request returns it (still `in_review` when approved directly during a review).
    
-   [`approval_request.changes_requested`](https://public-api.dapi.digital/docs/reference/events/approval_request.changes_requested.md)
    
    Integrators only. 3DAPI completed the review of your approval request and rejected some files, so your account is not approved yet. Each rejected file in `quotes` has a `reason`; upload corrected files and send a new request (get\_integrator\_onboarding says how many). You get a fresh upload allowance.
    

## Endpoints and the events log

Add endpoints on the Webhooks page of your dashboard: several per account, each with its own events and signing secret (shown once, on creation and on rotation), at most 10 per account, enabled or not. The dashboard also sends test events and shows every delivery with your server's answers. The events log keeps the last 30 days of events, delivered or not; each event shows where it was sent and the API request that caused it, and can be sent again with the same event id. Reading endpoints and events with an API key needs the `webhooks:read` scope; managing them needs a dashboard session.

-   [GET List events `/v2/events`](https://public-api.dapi.digital/docs/reference/webhooks.md#list_events)
-   [GET Get an event `/v2/events/{id}`](https://public-api.dapi.digital/docs/reference/webhooks.md#get_event)
-   [GET List webhook endpoints `/v2/webhook-endpoints`](https://public-api.dapi.digital/docs/reference/webhooks.md#list_webhook_endpoints)
-   [POST Create a webhook endpoint `/v2/webhook-endpoints`](https://public-api.dapi.digital/docs/reference/webhooks.md#create_webhook_endpoint)
-   [PATCH Update a webhook endpoint `/v2/webhook-endpoints/{id}`](https://public-api.dapi.digital/docs/reference/webhooks.md#update_webhook_endpoint)
-   [DELETE Delete a webhook endpoint `/v2/webhook-endpoints/{id}`](https://public-api.dapi.digital/docs/reference/webhooks.md#delete_webhook_endpoint)
-   [POST Rotate an endpoint secret `/v2/webhook-endpoints/{id}/rotate-secret`](https://public-api.dapi.digital/docs/reference/webhooks.md#rotate_webhook_endpoint_secret)
-   [POST Send a test event `/v2/webhook-endpoints/{id}/test`](https://public-api.dapi.digital/docs/reference/webhooks.md#send_test_webhook_event)
-   [GET List deliveries to an endpoint `/v2/webhook-endpoints/{id}/deliveries`](https://public-api.dapi.digital/docs/reference/webhooks.md#list_webhook_endpoint_deliveries)
-   [GET Get where an event was sent `/v2/events/{id}/deliveries`](https://public-api.dapi.digital/docs/reference/webhooks.md#get_event_deliveries)
-   [POST Send an event again `/v2/events/{id}/resend`](https://public-api.dapi.digital/docs/reference/webhooks.md#resend_event)

---

3DAPI API Docs. This page on the web: https://public-api.dapi.digital/docs/guides/webhooks. Index of every page: https://public-api.dapi.digital/docs/llms.txt.
