For the complete documentation index, see llms.txt. This page is also available as Markdown.

Webhooks

Stay informed about important events in your Duro library with real-time webhook notifications.

Webhooks follow a "Ping then Pull" pattern - you receive lightweight event notifications with key metadata, then fetch full resource details using our GraphQL API when needed.

Overview

Duro webhooks let your applications receive real-time notifications when important events occur in your library. Instead of constantly polling for changes, webhooks push event notifications directly to your specified endpoints, helping you build responsive integrations that react immediately to data changes.

Key Benefits

  • Real-time updates - Get notified instantly when data changes

  • Efficient integration - No need for constant API polling

  • Selective subscriptions - Subscribe only to the events you care about

  • Reliable delivery - Built-in retry mechanisms with exponential backoff

  • Secure - HMAC-SHA256 signature verification to ensure authenticity

How Webhooks Work

  1. Event occurs - Something happens in Duro (e.g., a component is updated)

  2. Notification sent - Duro sends a lightweight JSON payload to your webhook URL

  3. Fetch full data - Your application uses the provided metadata to fetch complete details via GraphQL as needed

This pattern keeps webhook payloads small and fast while giving you access to all the data you need.


Available Events

Webhooks are scoped to a specific library. Each webhook can subscribe to one or more event types.

Component Events

These events fire when components in your library are created, updated, or deleted:

GraphQL Enum
Payload Value
Description

COMPONENT_CREATED

components.created

A new component was created in the library

COMPONENT_UPDATED

components.updated

An existing component was modified

COMPONENT_DELETED

components.deleted

A component was removed from the library

When subscribing to events via GraphQL, use the enum name (e.g., COMPONENT_CREATED). When processing webhook payloads, the event field contains the string value (e.g., components.created).

Change Order Events

Track the full lifecycle of change orders in your library with these events:

GraphQL Enum
Payload Value
Description

CHANGE_ORDER_OPENED

change_orders.opened

A change order transitioned from draft to open status

CHANGE_ORDER_UPDATED

change_orders.updated

Change order details were modified (name, description, etc.)

CHANGE_ORDER_DELETED

change_orders.deleted

A change order was archived

CHANGE_ORDER_STAGE_TRANSITION

change_orders.stage_transition

Change order moved to a different workflow stage

CHANGE_ORDER_STAGE_REVIEWER_DECISION

change_orders.stage_reviewer_decision

A reviewer approved or rejected their stage

CHANGE_ORDER_RESOLUTION

change_orders.resolution

Change order was fully approved, rejected, or withdrawn

CHANGE_ORDER_CLOSED

change_orders.closed

A change order transitioned into the terminal closed state after resolution

Semantic Event Priority: When a change triggers multiple events (e.g., updating status from draft to open), Duro sends the more specific semantic event (CHANGE_ORDER_OPENED) rather than the generic CHANGE_ORDER_UPDATED. This prevents duplicate notifications and makes event handling more predictable.


Setting Up Webhooks

Create a Webhook

To create a webhook, you'll need:

  • The x-library header set to specify which library to monitor

  • A url - your HTTPS endpoint that will receive notifications

  • A list of events - which event types to subscribe to

Required Headers: All webhook operations require the x-api-key, x-organization, and x-library headers. The webhook will be created for the library specified in the x-library header.

Configuration Options

Field
Required
Description
Default

name

Yes

Unique name for this webhook (1-100 characters)

-

url

Yes

HTTPS endpoint URL (must be valid HTTPS)

-

events

Yes

Array of event types to subscribe to

-

description

No

Optional description (max 500 characters)

null

signingSecret

No

Secret for HMAC signature verification (min 16 characters if provided)

null

timeoutSeconds

No

Request timeout in seconds (5-300)

30

maxRetries

No

Maximum retry attempts on failure (0-10)

3

isEnabled

No

Whether the webhook is active

true

isArchived

Read-only

Whether the webhook has been archived (set via archive mutation)

false

List Your Webhooks

Retrieve all webhooks for the library specified in your x-library header:

The findAll query returns webhooks for the library specified in the x-library header. Only active (non-archived) webhooks are returned.

Get a Specific Webhook

The findOne query returns a webhook_not_found error if the webhook has been archived.

Update a Webhook

You can update any webhook configuration field:

Add Events to a Webhook

Add additional event subscriptions without removing existing ones:

Remove Events from a Webhook

Remove specific event subscriptions:

Archive a Webhook

When you no longer need a webhook but want to preserve its configuration history, you can archive it instead of deleting it. Archived webhooks:

  • Stop receiving events - No new event deliveries will be attempted

  • Are hidden from queries - Won't appear in findAll or findOne results

  • Free up the name - You can create a new webhook with the same name

  • Cannot be unarchived - This action is permanent

When to Archive vs. Disable

Action
Use Case

Disable (isEnabled: false)

Temporarily pause deliveries; webhook remains queryable and can be re-enabled

Archive

Permanently retire a webhook; frees up the name for reuse


Webhook Payloads

All webhook notifications follow a consistent JSON structure:

Payload Fields Explained

Field
Type
Description

event

string

The event type (e.g., components.created)

eventId

string

Unique identifier for this webhook delivery (UUID)

sourceId

string

Internal event ID for tracking and debugging

timestamp

string

ISO 8601 timestamp when the event occurred

metadata

object

Event-specific data (see below)

Component Event Metadata

For component events, the metadata object contains:

Field
Type
Description

componentId

string

UUID of the affected component

revisionValue

string

Current revision value (e.g., "1.A", "2.B")

version

number

Current version number

Example: Component Created

Example: Component Updated

Change Order Event Metadata

Each change order event type includes different metadata fields based on the event context.

change_orders.opened

Fired when a change order transitions from draft to open status (submitted for review).

Field
Type
Description

changeOrderId

string

UUID of the change order

status

string

New status value (open)

change_orders.updated

Fired when change order details are modified (name, description, etc.) without triggering a semantic event like opened or resolution.

Field
Type
Description

changeOrderId

string

UUID of the change order

change_orders.deleted

Fired when a change order is archived.

Field
Type
Description

changeOrderId

string

UUID of the change order

change_orders.stage_transition

Fired when a change order moves between workflow stages.

Field
Type
Description

changeOrderId

string

UUID of the change order

previousStageId

string

UUID of the previous stage (omitted if starting workflow)

newStageId

string

UUID of the new stage (omitted if resolved/completed)

transitionReason

string

Reason for the transition (e.g., stage_approved, stage_rejected)

change_orders.stage_reviewer_decision

Fired when a reviewer makes a decision (approve/reject) on their assigned stage.

Field
Type
Description

changeOrderId

string

UUID of the change order

stageId

string

UUID of the stage being reviewed

reviewerId

string

UUID of the reviewer who made the decision

decision

string

The decision made (approved or rejected)

change_orders.resolution

Fired when a change order reaches a final resolution (approved, rejected, or withdrawn).

Field
Type
Description

changeOrderId

string

UUID of the change order

resolution

string

Final resolution (approved, rejected, or withdrawn)

change_orders.closed

Fired when a change order transitions into the terminal closed state after it has been resolved. This is distinct from change_orders.resolution: resolution records the decision (approved/rejected/withdrawn), while closed marks the change order moving into its final closed status. Both events fire for a given close.

Field
Type
Description

changeOrderId

string

UUID of the change order

sequentialId

number

Human-facing sequential number of the change order (e.g. 2 for CO-2)

libraryId

string

UUID of the library the change order belongs to

status

string

Always closed for this event

resolution

string

Resolution that preceded the close (approved, rejected, or withdrawn)

resolvedAt

string

ISO 8601 timestamp of when the change order was resolved

closedAt

string

ISO 8601 timestamp of when the change order was closed

closedByUserId

string

UUID of the user who closed the change order

HTTP Headers

Each webhook request includes these headers:

Header
Value

Content-Type

application/json

User-Agent

Duro-Webhook-Service/1.0

X-Webhook-Signature

Versioned HMAC-SHA256 signature in the form sha256=<hex digest> (only sent when a signingSecret is configured on the webhook)


Signing Secrets

Every Duro webhook can carry a signing secret that lets your endpoint cryptographically verify each delivery actually came from Duro and was not modified in transit. Treat the signing secret like any other credential — it is the only thing protecting your endpoint from spoofed requests.

How Signing Works

When a webhook has a signingSecret, Duro signs every outbound delivery as follows:

  1. Compute HMAC_SHA256(rawRequestBody, signingSecret) and hex-encode the digest.

  2. Send the result in the X-Webhook-Signature header, prefixed with the algorithm version.

The header value uses a versioned, Stripe-style format:

The sha256= prefix is reserved for future algorithm upgrades — your verification code should split on = and reject any prefix it does not understand rather than assume a bare hex digest.

The signature is computed over the exact bytes of the request body that Duro sent. Re-serializing the JSON (for example by parsing and re-stringifying it) will produce a different signature and verification will fail. Always verify against the raw body.

Providing a Signing Secret

The signingSecret field is optional on webhooks.create and webhooks.update:

  • If you provide a signingSecret, Duro stores the value you provided (minimum 16 characters).

  • If you omit signingSecret on create, Duro generates a cryptographically secure signing secret for you by default. You can read it back via the findOne query.

  • If a webhook has no signing secret configured at all, deliveries are sent unsigned and the X-Webhook-Signature header is omitted.

Because Duro auto-generates a signing secret on create, in practice every new webhook is signed unless you have deliberately cleared the secret.

Verifying Signatures

Your endpoint should:

  1. Read the raw request body bytes before any JSON parsing.

  2. Read the X-Webhook-Signature header and split it into algorithm + digest on the first =.

  3. Compute HMAC_SHA256(rawBody, signingSecret) and hex-encode it.

  4. Compare the computed digest to the digest from the header using a timing-safe comparison.

  5. Reject the request if the header is missing, the algorithm is unknown, or the digests do not match.

Verifying Signatures (Node.js)

Verifying Signatures (Python)

Endpoint Hardening Checklist

  • Reject unsigned requests when you expect signatures. If your webhook has a signing secret configured, treat a missing X-Webhook-Signature header as an authentication failure (401).

  • Verify against the raw body. Parsing JSON before verifying will change the byte sequence and break the signature.

  • Use a constant-time comparison. Use crypto.timingSafeEqual (Node) or hmac.compare_digest (Python) — never === / ==.

  • Pin the algorithm. Refuse any header that does not start with sha256= so a future algorithm migration is explicit.

  • Dedupe with eventId. Signature verification proves authenticity, not uniqueness — combine it with idempotent processing keyed on eventId.

Security Best Practices

  • Always use HTTPS - Webhook URLs must use HTTPS (enforced by Duro)

  • Verify signatures - Always validate the X-Webhook-Signature header

  • Use timing-safe comparison - Prevent timing attacks when comparing signatures

  • Keep secrets secure - Store your signingSecret in environment variables, never in code

  • Rotate secrets periodically - Update your signing secret via webhooks.update regularly


Fetching Full Resource Data

After receiving a webhook notification, use the provided IDs to fetch complete resource details via GraphQL.

Fetch Component Details

Fetch Change Order Details

Example: Complete Webhook Handler

Here's a complete example showing how to receive a webhook and fetch the full component data:


Retry Logic & Error Handling

Duro automatically retries failed webhook deliveries using exponential backoff.

Retry Schedule

A delivery is retried up to maxRetries times (the webhook's configured value, 010, default 3) after the initial attempt. Each attempt is given timeoutSeconds + 10 to respond before it counts as a timeout.

Delays grow exponentially (2× multiplier) starting at 60 seconds, capped at 5 minutes:

Retry
Delay before retry

1st retry

60 seconds

2nd retry

120 seconds (2 min)

3rd retry

240 seconds (4 min)

4th retry

300 seconds (5 min — cap)

5th–10th retry

300 seconds (5 min — cap)

With the default maxRetries: 3, a failing delivery is attempted 4 times total (1 initial + 3 retries) over roughly 7 minutes of backoff before the log is marked FAILED.

Not every failure is retried. If the webhook is disabled or archived (or its log row no longer exists), the delivery fails immediately and does not consume retries.

What Counts as Success?

  • Success: HTTP status codes 200-299

  • Failure: All other status codes, timeouts, or connection errors

Your Endpoint Should

  1. Respond quickly - Return a 200 status code immediately, then process asynchronously

  2. Handle duplicates - Use eventId for idempotency; you may receive the same event more than once

  3. Log failures - Track and investigate webhook processing failures

  4. Be available - Ensure your endpoint is highly available to receive webhooks

Example: Idempotent Processing


Monitoring Webhook Activity

Every delivery attempt is recorded as a webhook log. Use the filterLogs query to monitor delivery health, build dashboards, audit activity over a time window, or drive reprocessing workflows.

The filterLogs query

filterLogs returns a Relay-style connection: a page of edges (each wrapping a WebhookLog node and its pagination cursor), a pageInfo block, and a totalCount of all logs matching your filter.

Filter input structure

The input is a WebhookLogFilterInput:

Field
Type
Description

and

[WebhookLogFilterConditionInput!]

Conditions that must all match (logical AND)

or

[WebhookLogFilterConditionInput!]

Conditions where any may match (logical OR)

pagination

PaginationInput

Page size, cursor, and sort order

Each entry in and / or is a WebhookLogFilterConditionInput:

Condition field
Operator type
Filters on

webhookId

UUIDFilterInput

The webhook a log belongs to

createdAt

DateFilterInput

When the log row was created (event received)

updatedAt

DateFilterInput

When the log row was last modified (last delivery attempt / processing update)

isAcknowledged

BooleanFilterInput

Whether your endpoint returned a 2xx (true = delivered)

processedState

WebhookLogProcessedStateFilterInput

Your integration's processing outcome (see markProcessed)

status and event are returned on the log node but are not filterable server-side. To surface undelivered logs, filter on isAcknowledged: { eq: false } (matches PENDING and FAILED); to narrow by event type, return event and filter client-side.

Filter operators

processedState uses standard SQL three-valued logic: neq: FAIL excludes both FAIL rows and unprocessed (null) rows. To match "anything that isn't FAIL, including not-yet-processed," combine neq: FAIL with isNull: true in an or.

Pagination

PaginationInput is cursor-based and forward-only:

Field
Type
Description

first

Int

Maximum number of logs to return in this page

after

String

Return logs after this cursor — pass the previous page's pageInfo.endCursor

orderBy

GenericOrderInput

{ field: String!, direction: ASC | DESC } — e.g. { field: "createdAt", direction: DESC }

Walk through all pages by repeating the query with after: <endCursor> until pageInfo.hasNextPage is false. hasPreviousPage is always false in v1.

The webhook log node

A WebhookLog carries three groups of fields:

Delivery (managed by Duro — did we reach your endpoint?)

Field
Type
Description

status

WebhookStatus!

PENDING, DELIVERED, FAILED (RETRYING is legacy and no longer written)

isAcknowledged

Boolean!

true once your endpoint returns a 2xx

responseCode

Float

HTTP status code returned by your endpoint

responseTimeMs

Float

Round-trip time of the delivery request

attemptCount

Float!

Number of delivery attempts made

deliveryErrorMessage

String

Last delivery error (timeout, connection error, non-2xx)

nextRetryAt

DateTime

When the next retry is scheduled, if retrying

Processing (managed by you via markProcessed)

Field
Type
Description

processedState

WebhookLogProcessedState

SUCCESS, FAIL, or null (not yet processed)

processedAt

DateTime

When you reported the processing outcome

processingErrorMessage

String

Failure reason you supplied when reporting FAIL

Identity & content

Field
Type
Description

id

ID!

Log ID — pass this to markProcessed

webhookId

ID!

The webhook that produced this log

event

String!

Event type (e.g. components.created)

payload

JSONObject!

The exact payload delivered to your endpoint

createdAt / updatedAt

DateTime!

Row timestamps

The errorMessage field is deprecated — it is an alias for deliveryErrorMessage. Use deliveryErrorMessage going forward.

Examples

Find undelivered logs across all your webhooks (anything that hasn't received a 2xx):

Audit a reporting window (all logs for one webhook in May 2026):

Find a backlog of unprocessed deliveries (delivered to you, but your integration hasn't reported an outcome — pairs with markProcessed):

Find deliveries your integration failed to process (for reprocessing / dead-letter handling):

Example: Monitoring Script


Tracking Processing Outcomes with markProcessed

A log's status (DELIVERED / FAILED) reflects whether Duro successfully delivered the HTTP POST to your endpoint and received a 2xx. It says nothing about whether your integration successfully processed the event afterward — your ERP sync might reject the part, your job queue might fail, your downstream API might be down.

markProcessed lets you record that application-side outcome back onto the webhook log as a processedState (SUCCESS or FAIL), with an optional failure reason.

markProcessed is optional — webhooks work exactly the same whether or not you call it. Its value is that it pairs with filterLogs: once you report processing outcomes, you can query for unprocessed or failed-processing logs to build reconciliation, retry, and dead-letter workflows entirely off Duro's log store — no separate tracking database required.

Reporting a successful outcome

Reporting a failure

Supply a processingErrorMessage so the reason is visible in later filterLogs queries:

Input fields (WebhookMarkProcessedInput)

Field
Required
Description

id

Yes

The WebhookLog id to mark

state

Yes

SUCCESS or FAIL

processingErrorMessage

No

Failure reason when state: FAIL. Persisted on the log; capped at 4000 characters — truncate stack traces client-side

Delivery status vs. processed state

These are two independent dimensions of a single log:

Owned by
Field(s)
Question it answers

Delivery

Duro

status, isAcknowledged, responseCode

Did Duro reach your endpoint and get a 2xx?

Processing

You (markProcessed)

processedState, processedAt, processingErrorMessage

Did your integration successfully handle the event?

A log can be DELIVERED (Duro's POST succeeded) yet processedState: FAIL (your ERP sync threw) — that combination is exactly what reconciliation workflows look for.

Use cases

  • Reconciliation backlog — report SUCCESS after each event is fully handled, then periodically query filterLogs(processedState: { isNull: true }, isAcknowledged: { eq: true }) to find delivered-but-not-yet-processed events your system may have dropped (a crash between receiving and processing).

  • Reprocessing / dead-letter — on a processing failure, call markProcessed(state: FAIL, processingErrorMessage: ...), then run a retry job over filterLogs(processedState: { eq: FAIL }).

  • Auditing & SLAprocessedAt gives an authoritative "when did the integration finish" timestamp alongside Duro's delivery timestamps, without standing up your own tracking store.

Example: receive → process → report


Common Use Cases

ERP Integration

Sync component and BOM changes to your ERP system in real-time:

Slack Notifications

Alert your team about important component changes:

Audit Logging

Maintain a detailed audit trail of all changes:

Change Order Workflow Tracking

Monitor change order progress and sync approval status to external systems:


Troubleshooting

Common Issues

Webhook not receiving events

  1. Check if webhook is enabled - Query the webhook and verify isEnabled: true

  2. Verify event subscriptions - Ensure the correct events are in the events array

  3. Check your endpoint - Verify your URL is accessible from the internet

  4. Review logs - Use the filterLogs query to see delivery attempts and errors (filter on webhookId and isAcknowledged: { eq: false } to surface undelivered events)

Signature verification failing

  1. Check the secret - Ensure you're using the exact same signingSecret you configured on the webhook

  2. Use the raw body - Signature is computed over the raw request bytes, not a re-serialized JSON object. Any middleware that parses or rewrites the body before verification will break the signature

  3. Strip the sha256= prefix - The header value is sha256=<hex>, not a bare hex digest. Split on the first = and compare only the hex portion

  4. Match digest lengths first - crypto.timingSafeEqual throws when buffers differ in length; guard with a length check before comparing

  5. Check encoding - Ensure UTF-8 encoding throughout

Missing webhook deliveries

  1. Check retry status - Some deliveries may be queued for retry

  2. Verify library scope - Webhooks only fire for events in their configured library

  3. Check component filters - Events fire for all components in the library

Testing Your Webhook Endpoint

Before configuring a production webhook, test your endpoint:


Migrating from v1 to v2

v1 webhook payloads included complete resource data. v2 uses a "Ping then Pull" pattern—you receive minimal metadata and fetch full details via GraphQL when needed.

Event Mapping

v1 Event
v2 Event
Notes

co.Submitted

CHANGE_ORDER_OPENED

co.Approved

CHANGE_ORDER_RESOLUTION

Check metadata.resolution === 'approved'

co.Rejected

CHANGE_ORDER_RESOLUTION

Check metadata.resolution === 'rejected'

Example: Change Order Approved

v1 payload (complete data embedded):

v2 payload (metadata only):

What's Not Included in v2

The following v1 fields are not in the v2 webhook payload. Use the GraphQL API to retrieve this data:

v1 Field
How to Get in v2

con, name, description

Query changeOrders.get()

status, type, resolution

Query changeOrders.get()

creator, firstName, lastName, email

Query changeOrders.get() with createdBy field

approverList

Query changeOrders.get() with reviewers field

children.components, children.products

Query changeOrders.get() with items field

history

Query change order audit log

created, lastModified

Query changeOrders.get() with createdAt, updatedAt

Updating Your Handler

v1:

v2:

See Fetch Change Order Details for the GraphQL query and Signing Secrets for signature verification.


Next Steps

Last updated

Was this helpful?