Error Handling
Learn how to handle errors and edge cases in the Duro API.
Error Types
The API returns different types of errors:
Validation errors
Authentication errors (see Authentication Errors — these use a different envelope)
Authorization errors
Entitlement (subscription plan) errors
Rate limiting errors
Server errors
Error Format
Errors arrive in the standard GraphQL errors array. extensions.code is the stable, programmatic key — branch on it rather than on message, which may be reworded.
{
"errors": [
{
"message": "Unauthorized",
"path": ["user"],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": { "message": "Unauthorized", "statusCode": 401 },
"service": "foundation"
}
}
]
}There are two shapes to be aware of, and code is the only key present in both:
Framework errors — authentication and authorization failures raised before your operation runs (
UNAUTHENTICATED,FORBIDDEN). The HTTP status is nested underextensions.originalError.statusCode, as above.Domain errors — business-rule violations raised by the operation itself. Every code documented on this page is one of these. They carry
statusCodedirectly onextensions, with nooriginalError:
service names the subgraph that raised the error. A few domain errors carry extra keys; those are called out alongside the code below.
Rate Limiting Errors
When an operation's budget is empty, the API refuses the whole request with HTTP 429 Too Many Requests before running any of it. The body names the budget and how many seconds to wait, and the Retry-After header carries the same wait:
This refusal comes from in front of the API rather than from a subgraph, so it carries no path, service or statusCode. Wait extensions.retryAfter seconds and send the request again unchanged: nothing in it ran. See Rate Limits for how budgets work, and Handling 429s for a retry helper.
Authentication Errors
Errors from the /auth/* account APIs — signup, sign-in, sessions, password reset, SSO — do not use the GraphQL envelope. They are REST endpoints, and they return a flat JSON body:
The code is stable; the message is copy and may be reworded. Branch on code.
AUTH_ORIGIN_REJECTED
403
Missing or non-allowlisted Origin header. Browsers send it automatically; curl does not
AUTH_TOKEN_INVALID
400
A verification or reset token that is unknown, expired, or already used
AUTH_RATE_LIMITED
429
Far past the route's limit. The body carries retryAfter (seconds) and the response a matching Retry-After header. See Sign-in is limited differently
AUTH_REQUEST_REJECTED
4xx
Malformed request — a missing field, a bad email shape, or a rejected password
AUTH_UNAVAILABLE
503
Temporary failure. Retry
Some codes deliberately cover more than one situation, so that the error surface cannot be used to work out who has a Duro account. See Creating an Account.
Access Errors
Authenticating identifies who you are. A separate check decides whether that identity may use Duro at all — see user.me.permitted. A valid token whose identity is not permitted is refused at every data operation with ACCESS_NOT_PERMITTED. The token itself is fine; the account is not yet allowed in.
ACCESS_NOT_PERMITTED
403
The identity is authenticated but not permitted: it is archived, or it has no organization membership, no pending invitation, no allowlisted email or domain, and no pending organization-creation grant. Nothing was returned.
Query user.me { permitted } to confirm. The account needs to be invited to an organization, or its email or domain approved by Duro. Once it is permitted the same request succeeds with the same token.
Validation Errors
Validation errors are returned when a mutation violates a business rule. The extensions.code identifies the specific rule so you can handle it programmatically.
NRM_NOT_ALLOWED_ON_OBSOLETE
Component update
A component cannot be marked Not Revision Managed (NRM) while it is Obsolete. Change the status away from Obsolete before enabling NRM. See Not Revision Managed.
Item Write Errors
item.create and item.update write a component together with its BOM children and its documents in a single transaction — see Item Mutations. Because the whole call is one unit of work, an oversized request is refused up front rather than partially applied.
ITEM_WRITE_TOO_LARGE
item.create, item.update
The request exceeds one of the write limits below. The check runs before any work starts, so nothing was written. HTTP 413.
The limits (per call unless stated):
Components in one inputs array
10
children on one component
20
children summed across the call
200
documents on one component
20
documents summed across the call
20
documentComponentIds on one component
20
removeDocumentLinkIds on one component (item.update only)
20
The message names the limit that was hit:
How to resolve: split the work into several calls, one per component rather than a large batch, and move a component's documents into a follow-up item.update if the component alone is still too large. Do not split a single component's children across calls — the list is desired-state, so a partial list unlinks the children left out of it. A parent with more than 20 children cannot be published through item.* at all: create it with no children, then send the full BOM through assemblyMutation.updateBOM, which has no such cap. Retrying the same request unchanged will not succeed.
Document Link History Errors
Adding or removing a document link on its own — outside a change order and outside an item.* call — is a single transaction: the link write and the history row that records it either both land or neither does.
DOCUMENT_HISTORY_WRITE_FAILED
standalone document link add / remove
The link change could not be recorded in the component's history, so the whole save was rolled back. Nothing was written. HTTP 500.
How to resolve: the failure is transient on the server side, not a problem with the request. Retry the same call. Because the save rolled back, retrying cannot produce a duplicate link or a duplicate history row. If it persists, contact support with the component and document IDs.
Change Order Template Errors
Uploading a workflow template (changeOrders.createTemplate / changeOrders.updateTemplate) runs a set of checks on the YAML before it is stored. Participant checks (TEMPLATE_PARTICIPANT_*) and change order type checks run for every version. Structural validation against the versioned JSON Schema runs only for versions listed as enforced in the version manifest, which today is 1.2.
TEMPLATE_CONFIG_INVALID
Returned when a 1.2 template fails validation against schema-1.2.json. The check runs on template create and update, and when a library's built-in templates are seeded. 1.0 and 1.1 templates are not validated against their schema, so they never return this code.
HTTP 400. The message gives the location of the problem as a JSON Pointer into the config, then the reason:
The reason is one of:
Missing required field: <name>
A required property is absent, for example a stage without name.
Invalid type: expected <type>
A value has the wrong type, such as a string where an array belongs.
Invalid format: <detail>
A string does not match its pattern, such as a reviewer ID that is not a lowercase UUID.
Invalid value: must be one of [<values>]
A value is outside its allowed set, such as an unknown approval type.
Field is not allowed at this schema version
The key exists in the format but not at the version the template declares.
How to resolve:
Follow the pointer in the message to the key (
/stages/open/0/typesabove is thetypesof the first open stage) and fix it in your YAML.Validate locally against
schema-1.2.jsonbefore uploading. The Workflow YAML Reference shows how to wire the schema into your editor.Unknown keys are not a cause of this error; the schemas allow them.
A template cannot be moved back from
1.2to an earlier version to avoid validation; that is rejected withCANNOT_DOWNGRADE_SCHEMA_VERSION.
Change Order Type Errors
Selecting a change order type can produce validation errors when the requested type is not permitted, or when a DCO (Documentation Change Order) is asked to do something its revision-freeze forbids.
CO_TYPE_NOT_ALLOWED
Returned when a change order is given a coType that the chosen template does not allow. A template's allowed types are declared in its co_types list (schema version 1.1). The resolved type — whether passed explicitly in the input, taken from the template's defaultCoType, or defaulted to ECO — must be a member of that list.
This applies both at creation (changeOrders.create) and when switching a draft change order's type or template with changeOrders.submitDraft (coType / configId) — see Switching Type and Template in Draft. When both fields are passed to submitDraft, the type is validated against the new template.
How to resolve:
Query the template's allowed types before creating and pass a
coTypethat is a member ofcoTypes(or omitcoTypeto fall back todefaultCoType):Legacy (
1.0) templates returnnullforcoTypesand allow onlyECO. To offer additional types, migrate the template toversion: "1.2"— see the Workflow YAML Reference.
DCO_STATUS_CHANGE_NOT_ALLOWED / DCO_REVISION_CHANGE_NOT_ALLOWED
A change order of type DCO is revision-frozen: it never bumps a component's revision and cannot change a component's status or revision. These codes are raised whenever a DCO would result in such a change. If the change genuinely needs to alter a component's status or revision, use a non-DCO type. See Change Order Types.
They surface in three situations:
Proposed change. Proposing a status change or a revision change directly on a DCO item is rejected.
Baseline drift at submit. The built-in DCO baseline validation blocks the change order when any item's live
statusorrevisionhas drifted from its last-released baseline — for example because a concurrent change order released a new revision of that item after it was added to the DCO.Release-time abort at close. The release path re-checks the baseline when the DCO closes and aborts with these codes if drift is detected then, or if a DCO item has no releasable baseline or its live component can't be found.
Required Approver Errors
From template version: '1.2', a stage reviewer can be a required approver. These errors protect that guarantee. All three return HTTP 400.
INLINE_CONFIG_REVIEWER_OBJECTS_UNSUPPORTED
Returned when changeOrders.create is called with an inline config (a workflow definition passed directly in the input rather than a saved template) in which a stage's reviewers.users contains reviewer objects, such as { id: "<uuid>", isRequiredToApprove: true }. Inline configurations only accept reviewers as UUID strings. Required approvers can only come from a saved template.
How to resolve:
If you do not need required approvers, pass the reviewers as plain UUID strings in the inline
config:If you need required approvers, save the configuration as a
1.2template withchangeOrders.createTemplateand create the change order from that template instead. See Change Order Workflows and Getting Available Templates.
REQUIRED_APPROVER_UNRESOLVABLE
Returned when a change order is created from a template, or a draft change order is switched to one, and a required approver on the template can no longer be added as a reviewer in the library, for example because they have left the organization. Ordinary reviewers in that situation are skipped with an activity-feed warning; a required approver is not, because skipping them would let the stage pass without them. Nothing is created.
How to resolve: someone with permission to manage change order templates must update the template, replacing the required approver or removing their isRequiredToApprove flag. Retrying will not help until then.
CANNOT_REMOVE_REQUIRED_REVIEWER
Returned by removeReviewersFromStages when one of the reviewers to remove is a required approver on that stage. Required approvers cannot be removed in any change order status. The request is all-or-nothing: if any reviewer in it is required, no one is removed.
How to resolve: leave required approvers out of the removal request. To check first, read StageReviewer.isRequiredToApprove on the stage's reviewers. Whether someone is required is set by the template the change order was created from, not by the change order.
Change Order Export Errors
A change order export package is built asynchronously by an export job. When the job fails, Duro classifies the failure and records it on the job as a failureReason. Some reasons are retryable (a transient file-service or delivery problem, for example) and some are permanent — retrying will fail the same way until the underlying data is fixed.
failureReason
Retryable
Meaning
DOCUMENT_UNAVAILABLE
No
A document attached to the change order could not be fetched — typically its file is missing or is tagged with the wrong organization. The job's blockingDocument identifies it. See Documents.
CHANGE_ORDER_UNAVAILABLE
No
The change order the job was created for could no longer be read when the package was built.
JOB_MALFORMED
No
The job's stored parameters are invalid and cannot produce a package.
FILE_SERVICE_UNAVAILABLE, FILE_SERVICE_TIMEOUT
Yes
The file service was unreachable or too slow while downloading documents.
PACKAGING_FAILED, DELIVERY_FAILED
Yes
The archive could not be assembled or uploaded.
WORKFLOW_START_FAILED, TIMED_OUT, UNKNOWN
Yes
The export workflow did not start, exceeded its time budget, or failed for an unclassified reason.
Read retryable on the export job before offering a retry — it is derived from the reason above, and is true for jobs that failed before failure reasons were recorded. Both retryable and blockingDocument require library.read or exports.create.
EXPORT_CO_PERMANENT_FAILURE
Returned by retryChangeOrderExport when the job being retried failed for a permanent reason. extensions.failureReason carries the reason so you can tell the user what to fix.
How to resolve:
Fix the underlying data first — re-upload or correct the document named by
blockingDocument, or restore the change order.Then retry with
force: true. The guard is skipped and a new export job is started regardless of the previous failure's classification:
Organization Creation Errors
Creating a new production organization is gated: the account must hold a valid, unused organization-creation grant issued to its verified email address. Self-serve creation without a grant is not yet available. When a create is refused, extensions.code identifies why so you can present a clear, distinguishable message. Query Me.canCreateOrg beforehand to know whether to offer the action at all.
The codes below apply to the grant path. Creating a sandbox organization under an existing parent goes through a different set of checks — see Sandbox Organization Errors.
All five are 403.
GRANT_NONE
The refusal you should expect in practice. No currently claimable grant was found for the account. A grant that was never issued, has already been consumed, was revoked, has expired, or whose backing package is archived all collapse into this one code — the message is deliberately generic and never reveals whether a grant exists for some other account.
Request a grant from Duro before creating an organization.
SELF_SERVE_NOT_AVAILABLE
The account has no claimable grant but is on the self-serve allowlist — it would be eligible to create its own organization, except self-serve creation has not shipped yet. It is the more specific refusal for accounts that would otherwise be permitted, not a broader one.
Request an organization-creation grant from Duro in the meantime.
GRANT_ALREADY_CONSUMED
Race-only. The grant was pending when it was read, but a concurrent create claimed it first. A grant that was already consumed before the request began returns GRANT_NONE instead.
Treat as a lost race. Do not retry — the grant is spent. Request a new grant to create another organization.
GRANT_REVOKED
Race-only. The grant was revoked in the instant between being read and being claimed. A grant revoked before the request began returns GRANT_NONE instead.
Contact Duro to have a new grant issued.
GRANT_BACKING_PACKAGE_ARCHIVED
Defensive guard; not reachable on the normal path. Grants whose backing package is archived are filtered out before the claim, so they surface as GRANT_NONE.
Contact Duro; the grant must be re-issued against an active package.
Import Job Errors
These codes describe the import job as a whole, rather than an individual row. Per-row failures are reported on rowResults — see Row Outcomes and Sourcing Import Errors.
IMPORT_JOB_TERMINALIZED_DURING_PROMOTION
409
The job was moved to a terminal state — typically because it was cancelled — while its components were being committed. The promotion is rolled back, so no component from the sheet is written.
Nothing was partially applied. Re-upload the sheet if the import was still wanted.
Sourcing Import Errors
Columns mapped to a src: target are applied after the component rows are written. When a sourcing value cannot be applied, the failure is reported against the row with one of these codes.
Note that an unrecognized name is generally not an error: unknown manufacturers, distributors, and package types are created on demand. The codes below cover the cases where a name resolves to something the import will not act on, or cannot be resolved at all.
SOURCING_IMPORT_PACKAGE_TYPE_ARCHIVED
src:packageType
The value matches an archived package type. Matching is case-insensitive and covers archived package types, so the name cannot be re-created and the row is failed rather than silently reviving the archived type. Status 409.
Restore the package type in Duro, or use a different name.
SOURCING_IMPORT_PACKAGE_TYPE_AMBIGUOUS
src:packageType
The value matches more than one package type — two existing types differing only by case — so the import cannot choose one. Creating a third would not resolve which one the sheet meant.
Use the exact, unambiguous package name.
SOURCING_IMPORT_PACKAGE_TYPE_UNRESOLVABLE
src:packageType
The package type did not exist and could not be created — typically two rows of the same import racing on the same new name. Status 409.
Re-upload the sheet. The package type usually exists by then and the retry resolves it.
SOURCING_IMPORT_UNREADABLE_VALUE
src:packageType
The cell is longer than 100 characters, which is a mis-mapped column rather than a package name.
Check the column mapping, and shorten the value.
SOURCING_IMPORT_PART_IDENTITY_CHANGED
src:manufacturer / src:mpn
The manufacturer part the row matched changed identity, or stopped being available, between being matched and being written — typically because it was edited or removed concurrently. The row is failed rather than written to a part that is no longer the one it matched.
Re-upload the sheet. The row was not partially applied, and on a settled library the retry resolves the part again and succeeds.
Sourcing Query Errors
Returned by vendorSourcing.componentSourcingByVendor and the paged vendor list queries vendorSourcing.libraryVendors and archivedLibraryVendors. Write-side sourcing codes such as DUPLICATE_PART are listed on the Sourcing page.
COMPONENT_SOURCING_INVALID_CURSOR
The after argument is not a valid pagination cursor. Status 400.
Restart from the first page by omitting after, and only pass back an endCursor received from pageInfo.
VENDOR_LIST_INVALID_CURSOR
The pagination.after argument to libraryVendors or archivedLibraryVendors is not a valid cursor: it is malformed or empty, came from the other list query, or came from a different orderBy field or direction. The message is Invalid pagination cursor. Status 400.
Restart from the first page by omitting after, and only pass back an endCursor received from pageInfo of the same query with the same orderBy.
VENDOR_NOT_FOUND
The vendorId does not match a known vendor. A vendor with no matching sourcing rows returns an empty page, not this error; an archived vendor can still return rows. Status 404.
Check that the id is the vendor's id and that the vendor exists.
PERMISSION_NOT_FOUND
The caller lacks components.read in the library. The query still returns its rows, but each row's component is null and the response carries this error from the component service's permission check. Status 403. A caller without sourcing.vendor.read gets FORBIDDEN for the whole query instead.
Grant the caller's role components.read in that library, or don't select component.
Integration Access Errors
Writes made on behalf of an integration are checked against the principal model: open attributes are writable by anyone, managed attributes only by principals linked to the component. These codes are returned by component writes (including reconcile), by linkComponent / unlinkComponent, and by setAttributeAccess.
ATTRIBUTE_LOCKED_BY_INTEGRATION
The attribute is managed, and the caller is not one of its linked managers. Also covers the frozen case — a managed attribute with no linked manager at all is writable by no one.
Link the acting principal to the component and make it a manager of the attribute, or write a different attribute. Read effectiveAttributeAccess first to know which fields are editable.
ATTRIBUTE_REQUIRES_COMPONENT_LINK
The acting integration manages the attribute but is not linked to this component. Open attributes never raise this — only managed ones.
Call linkComponent for that component, then retry.
INTEGRATION_NOT_ENABLED_FOR_LIBRARY
The acting integration is not enabled for the library in the x-library header.
Enable the integration for that library, or target a library where it is enabled.
COMPONENT_NOT_FOUND
The referenced component does not exist in the current library context.
Check the id and the x-library header.
ATTRIBUTE_NOT_FOUND
The referenced attribute does not exist for the component's category in this library.
Re-read the category's attributes; ids are library-scoped.
INTEGRATION_CLAIM_NOT_AUTHORIZED
The caller may not claim the component for the requested integration.
Check the caller's integrations.configure permission and that the target integration is the one the request is acting as.
CLAIM_REQUIRES_ACTING_INTEGRATION
A claim or link was requested on a request that has no acting integration. Duro will not infer one.
Make the call with integration credentials so the request has an acting integration.
STORE_PRINCIPAL_ALWAYS_READS
An access policy tried to give the Duro principal no-read or no-access on an attribute. Duro must always be able to render its own data.
Use receive-only instead, which makes the attribute read-only in the web app while keeping it visible.
Two further codes guard exclusive attribute management — the EXCLUSIVE_WRITE write designation, which makes one principal the sole manager of an attribute. Both are returned by setAttributeAccess.
EXCLUSIVE_BLOCKED_BY_MANAGERS
EXCLUSIVE_WRITE was requested for a principal while other principals still hold a write designation on that attribute.
Clear the other principals' write designations first, then retry. Duro will not demote them for you.
EXCLUSIVE_MANAGER_EXISTS
A write designation was requested for a principal on an attribute another principal already manages exclusively.
Remove the existing exclusive designation first, or designate a different attribute.
Access Scope Errors
Some writes are governed per component by an access scope rather than per attribute. Two scopes are enforced: BOM (the lines of an assembly — adding, removing, swapping or re-quantifying them, by any route including reconcile and import) and DOCUMENTS (a component's document links, and pointing a document at it with depictsComponentId). See Access scopes and Who May Change a Component's Documents. Read scopeAccess on the component first to know whether the write will be accepted.
SCOPE_LOCKED_BY_INTEGRATION
The scope on this component is managed by an integration, and the caller is not that integration.
Make the change in the managing integration. The message names it.
SCOPE_REQUIRES_COMPONENT_LINK
The acting integration is designated to manage the scope but is not linked to this component.
Call linkComponent for that component, then retry.
SCOPE_WRITE_DISABLED_FOR_USERS
The library's policy makes the scope read-only for people, so the web app and user API keys cannot write it.
Make the change through the integration that manages the scope.
SCOPE_WRITE_BLOCKED_FOR_INTEGRATION
The library's policy explicitly blocks the acting integration from writing the scope.
Write through a principal that is allowed to, or have a library admin change the policy.
All four are 403, and the message names the scope. A library with no policy configured for a scope never returns them: the scope is open to every caller.
Entitlement Errors
Some features and limits are governed by your organization's subscription plan. When a request targets a feature your plan does not include, or would exceed a quota your plan allows, the API returns an entitlement error. Match on extensions.code — it is the stable, programmatic key.
FEATURE_NOT_IN_PLAN
The operation requires a feature that your organization's plan does not include. For example, configuring Enterprise SSO requires the SSO_SAML entitlement and SCIM provisioning requires the SCIM entitlement.
Contact your Duro account team to add the feature to your plan.
SEAT_LIMIT_REACHED
The operation would exceed the number of user seats your plan allows.
Remove unused members, or contact your Duro account team to raise your seat count.
ENTITLEMENT_LIMIT_REACHED
The operation would exceed another quota your plan allows (storage or sandbox organizations, for example). The message names the specific limit, and extensions.entitlement carries its machine key.
Reduce usage below the limit, or contact your Duro account team to raise it.
All three are 403. The messages interpolate the specific feature or counts, so branch on code — and, for ENTITLEMENT_LIMIT_REACHED, on extensions.entitlement.
Sandbox Organization Errors
Sandbox organizations are metered against the parent organization's sandbox_orgs entitlement. Pass parentOrganizationId to organization.create to request one. These codes can come back when a sandbox operation is refused.
SANDBOX_PARENT_FORBIDDEN
403
Create with parentOrganizationId
The chosen parent cannot host the sandbox. The parent must exist, be an active production (non-sandbox) organization that you are the Site Admin of, and have a subscription. Sandboxes cannot be nested, so a sandbox can never be a parent. Being an Org Admin rather than the Site Admin is not sufficient.
SANDBOX_NAME_TAKEN
409
Create with parentOrganizationId
A sandbox reuses its parent's company, so its name must be unique alongside the parent and every sibling sandbox. Unlike the other three, this one is user-fixable: prompt for a different name.
SANDBOX_NOT_AVAILABLE
409
Create with parentOrganizationId
Sandbox creation is not currently enabled. Refresh your creation options and retry — the request is refused rather than silently falling through to the grant path, so no grant is consumed.
SANDBOX_ARCHIVE_FORBIDDEN
403
organization.archiveSandboxOrganization
The target cannot be archived through this mutation. It only archives sandbox organizations you administer — production organizations, and sandboxes you do not administer, are rejected.
SANDBOX_PARENT_FORBIDDEN and SANDBOX_ARCHIVE_FORBIDDEN are deliberately generic: several distinct causes collapse into one message so the response never reveals whether a given organization exists or why exactly it was refused. Do not try to infer the specific cause from them.
Best Practices
Implement proper error handling
Add retry logic for rate limits that honours
Retry-After(see Handling 429s)Log errors for debugging
Handle network timeouts
Provide user-friendly error messages
Error Recovery
Next Steps
Join our Developer Community for support and discussions.
Last updated
Was this helpful?