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

API v2 Migration Guide

This guide provides comprehensive information for migrating your existing Duro API integrations to API v2. This is a breaking change release that introduces a new header-based context system and role-based access control (RBAC).

Overview

Why This Change?

The previous API suffered from inconsistent context handling:

  • Some operations required libraryId as a direct parameter

  • Others expected it within an input object

  • Some used library (slug) while others used libraryId (UUID)

  • Organization context was sometimes explicit, sometimes implicit

API v2 standardizes context through HTTP headers, eliminating redundancy and providing a cleaner, more predictable interface.

What's New?

Feature
v1 (Legacy)
v2 (New)

Library Context

Parameter in each operation

x-library header

Organization Context

Parameter in some operations

x-organization header

Access Control

Simple role enum

Full RBAC with permissions

API Key Scope

Tied to library at creation

Must match x-library header

Key Benefits

  1. Consistency: Every operation uses the same context mechanism

  2. Cleaner Queries: No redundant library/organization IDs in every request

  3. Fine-grained Permissions: New RBAC system enables granular access control

  4. Better Security: Permission checks on every request

  5. Simplified Client Code: Set headers once, use everywhere


Migration Checklist

Before starting your migration:


Required Headers

All authenticated API requests must include these headers:

x-organization (Required)

Identifies the organization context. Accepts either:

  • Organization UUID: 550e8400-e29b-41d4-a716-446655440000

  • Organization slug: @my-company

x-library (Required for library-scoped operations)

Identifies the library context. Accepts either:

  • Library UUID: 660e8400-e29b-41d4-a716-446655440000

  • Library slug path: @my-company/product-library

x-api-key (For API authentication)

Your API key for authentication (unchanged from v1).

Complete Request Example


Complete Breaking Changes Reference

This section provides exhaustive tables of every change grouped by category.


1. Input Objects: Removed Fields

Fields removed from input types - context now provided via headers.

Input Type
Removed Field(s)
Header to Use

BulkCreateReusableInstancesInput

libraryId: String!

x-library

CategoryFilterInput

libraryId: String

x-library

ComponentFilterInput

libraryId: String, libraryIds: [String!]

x-library

ConfigFilterInput

libraryId: String

x-library

ConfigureLibraryInput

libraryId: String!

x-library

CreateApiKeyInput

libraryId: String!

x-library

CreateComponentInput

libraryId: ID!

x-library

CreateLibraryInput

organizationId: String!

x-organization

CreateValidationRuleInput

libraryId: ID!

x-library

CreateWebhookInput

libraryId: String!

x-library

CustomStatusFilterInput

libraryId: String

x-library

DeactivateApiKeyInput

libraryId: String!

x-library

FindAllApiKeyForUserInput

libraryId: String

x-library

FindOneApiKeyForUserInput

libraryId: String

x-library

IdsFilterInput

libraryId: String

x-library

IsPinnedComponentInput

libraryId: ID!

x-library

NextRevisionInput

libraryId: String!

x-library

PinComponentsInput

libraryId: ID!

x-library

RevokeAllApiKeysForUserInput

libraryId: String!

x-library

RotateApiKeyInput

libraryId: String!

x-library

SetLibraryConfigInput

libraryId: String!

x-library

UnPinComponentsInput

libraryId: ID!

x-library

UpdateApiKeyInput

libraryId: String!

x-library

UpdateComponentInput

libraryId: ID

x-library

ValidateApiKeyInput

libraryId: String!

x-library

ValidateRevisionValueInput

libraryId: String!

x-library

ValidationRuleFilter

libraryId: ID

x-library

NaturalLanguageSearchInput

libraryIds: [String!]

x-library

Fields removed from input types with no replacement.

Input Type
Removed Field
Notes

CreateComponentInput

documentFormats: [String!]

UpdateComponentInput

documentFormats: [String!]

CreateItemInput

documentFormats: [String!]

UpdateItemInput

documentFormats: [String!]


2. Input Objects: Modified Fields

Fields changed or deprecated in input types.

Input Type
Field
v1 (Legacy)
v2 (New)

BulkInviteInput

role

role: OrganizationRoleEnum! (required)

role: OrganizationRoleEnum (optional, deprecated)

BulkInviteInput

roleId

(not present)

roleId: String (deprecated, use orgRoleId)

BulkInviteInput

orgRoleId

(not present)

orgRoleId: String (new, preferred)

BulkInviteInput

libraryRoles

(not present)

libraryRoles: [LibraryRoleAssignment!] (new)

UpdateApiKeyInput

id

(not present)

id: String (new, preferred selector)

UpdateApiKeyInput

apiKey

apiKey: String! (raw key or stored hash)

apiKey: String (optional, raw key only)

DeactivateApiKeyInput

id

(not present)

id: String (new, preferred selector)

DeactivateApiKeyInput

apiKey

apiKey: String! (raw key or stored hash)

apiKey: String (optional, raw key only)

RotateApiKeyInput

id

(not present)

id: String (new, preferred selector)

RotateApiKeyInput

apiKey

apiKey: String! (raw key or stored hash)

apiKey: String (optional, raw key only)


3. Root Query Changes

Changes to top-level Query fields.

Query
v1 Signature
v2 Signature
Change Type

altiumHealthCheck

altiumHealthCheck: AltiumHealthCheckResponse!

(removed)

REMOVED

customStatuses

customStatuses(filter: CustomStatusFilterInput!)

customStatuses(filter: CustomStatusFilterInput)

Filter now optional

configs

configs(filter: ConfigFilterInput!)

configs(filter: ConfigFilterInput)

Filter now optional

configSyncStatus

configSyncStatus(libraryId: String!, type: ConfigType!)

configSyncStatus(type: ConfigType!)

libraryId removed

libraries

libraries(organizationId: ID!)

libraries

organizationId removed

roles

(not present)

roles: RoleQueryNamespace!

NEW

tenantScopingProbe

(not present)

tenantScopingProbe: Boolean!

NEW

tenantScopingProbe is a lightweight, side-effect-free field for confirming that the default-deny tenant-scoping guard is active for your authenticated context. It returns true when a request passes the guard. See Tenant Scoping for details.


4. Root Mutation Changes

Changes to top-level Mutation fields.

Mutation
v1 Signature
v2 Signature
Change Type

generateAltiumViewerUrl

generateAltiumViewerUrl(input: GenerateViewerUrlInput!)

(removed)

REMOVED

membership

membership: MembershipMutationNamespace!

(removed from root)

Moved to organization.membership

roles

(not present)

roles: RoleMutationNamespace!

NEW


5. Namespace Query Method Changes

Changes to methods within query namespaces.

CategoryQueryNamespace

Method
v1 Signature
v2 Signature

findAll

findAll(libraryId: ID!, filter: CategoryFilterInput)

findAll(filter: CategoryFilterInput)

ChangeOrdersQueryOperations

Method
v1 Signature
v2 Signature

get

get(library: String, filter: ChangeOrderFilterInput, pagination: PaginationInput)

get(filter: ChangeOrderFilterInput, pagination: PaginationInput)

getTemplates

getTemplates(library: String)

getTemplates

ComponentQueryNamespace

Method
v1 Signature
v2 Signature

findOne

findOne(id: ID!): Component!

findOne(id: ID!): Component (now nullable)

validateAttributes

validateAttributes(categoryId: ID!, attributeValues: [AttributeValueInput!]!, libraryId: ID!)

validateAttributes(categoryId: ID!, attributeValues: [AttributeValueInput!]!)

getAllPinnedComponents

getAllPinnedComponents(libraryId: String!)

getAllPinnedComponents

IdentifierQueryNamespace

Method
v1 Signature
v2 Signature

validateOverrides

validateOverrides(libraryId: String!, overrides: [IdElementOverrideInput!]!)

validateOverrides(overrides: [IdElementOverrideInput!]!)

validateCompleteOverride

validateCompleteOverride(libraryId: String!, overrides: [IdElementOverrideInput!]!, componentId: String)

validateCompleteOverride(overrides: [IdElementOverrideInput!]!, componentId: String)

overridePermissions

overridePermissions(libraryId: String!)

overridePermissions

reusableGroupInstances

reusableGroupInstances(libraryId: String!, configId: String!, groupName: String!, filters: [ElementValueInput!])

reusableGroupInstances(configId: String!, groupName: String!, filters: [ElementValueInput!])

validateFreeformOverride

validateFreeformOverride(libraryId: String!, value: String!, componentId: String)

validateFreeformOverride(value: String!, componentId: String)

OrganizationQueryNamespace

Method
v1 Signature
v2 Signature

invitations

invitations(filter: InvitationFilterInput): [OrganizationInvitation!]!

invitations(filter: InvitationFilterInput): [Invitation!]!

discoverByDomain

(not present)

discoverByDomain: Organization

WebhookQueryNamespace

Method
v1 Signature
v2 Signature

findAll

findAll(input: FindWebhooksInput!)

findAll


6. Namespace Mutation Method Changes

Changes to methods within mutation namespaces.

ChangeOrdersMutationOperations

Method
v1 Signature
v2 Signature

create

create(input: CreateChangeOrderInput!, libraryId: ID)

create(input: CreateChangeOrderInput!)

createTemplate

createTemplate(input: CreateTemplateInput!, library: String)

createTemplate(input: CreateTemplateInput!)

ConfigMutationNamespace

Method
v1 Signature
v2 Signature

manuallyProcessCategoriesConfig

manuallyProcessCategoriesConfig(libraryId: String!)

manuallyProcessCategoriesConfig

OrganizationMutationNamespace

Method
v1 Signature
v2 Signature

membership

membership(organizationId: String!): MembershipMutationNamespace!

membership: MembershipMutationNamespace!

declineInvitation

(not present)

declineInvitation(id: String!): Boolean!

MembershipMutationNamespace

Method
v1 Signature
v2 Signature

update

update(userId: String!, role: OrganizationRoleEnum!)

update(userId: String!, roleId: String!)

updateLibraryAccess

(not present)

updateLibraryAccess(input: UpdateLibraryAccessInput!): Boolean!

removeOrgRole

(not present)

removeOrgRole(userId: String!): Boolean!


7. Enum Changes

OrganizationRoleEnum

v1 Values
v2 Values
Notes

SITE_ADMIN

SITE_ADMIN

Unchanged

ADMIN

ADMIN

Unchanged

USER

EDITOR

RENAMED

APPROVER

(removed)

REMOVED - use EDITOR

REVIEWER

REVIEWER

Unchanged

(not present)

VIEWER

NEW

SUPPLIER

SUPPLIER

Unchanged


8. Type Changes

Renamed Types

v1 Type
v2 Type
Notes

OrganizationInvitation

Invitation

Enhanced with RBAC fields

Modified Types

Type
Change

Organization

Added roles: [Role!]! field

Organization

Changed invitations return type to [Invitation!]!

OrganizationRole

Added rbacRole: Role, hasDirectOrgRole: Boolean!, libraryAccess: [LibraryAccessDto!]

Library

Added stats: LibraryStatsType field

ApiKey

Removed hashedKey field — see API keys: hashedKey removed

Component

Removed documentFormats field — see Component.documentFormats removed


9. Removed Types (Complete List)

Removed Type
Category

AltiumHealthCheckResponse

Altium integration

ViewerUrlResponse

Altium integration

GenerateViewerUrlInput

Altium integration

OrganizationInvitation

Renamed to Invitation

FindWebhooksInput

Webhooks (no longer needed)


10. New Types (Complete List)

New Type
Category
Purpose

Role

RBAC

Defines a role with permissions

Permission

RBAC

Individual permission definition

RoleType

RBAC

Enum: ORG, LIBRARY

RoleMutationNamespace

RBAC

Mutations for role management

RoleQueryNamespace

RBAC

Queries for role management

CreateRoleInput

RBAC

Input for creating custom roles

UpdateRoleInput

RBAC

Input for updating roles

DeleteRoleInput

RBAC

Input for deleting roles

Invitation

Invitations

Replaces OrganizationInvitation

LibraryAccessDto

RBAC

Library-specific role info

LibraryRoleAssignment

RBAC

Input for assigning library roles

LibraryStatsType

Library

Component/user counts

UpdateLibraryAccessInput

RBAC

Input for updating user library access


11. Code Examples (Before/After)

Quick reference examples for common operations.

Components

Change Orders

Libraries

Membership


Role-Based Access Control (RBAC)

API v2 introduces a comprehensive RBAC system that replaces the simple role enum approach.

Key Concepts

  1. Permissions: Granular capabilities like components.create, change_orders.approve

  2. Roles: Collections of permissions (e.g., Editor, Viewer, Admin)

  3. Role Types: Organization-level (ORG) or Library-level (LIBRARY)

  4. Hierarchical Resolution: Library roles override organization roles for specific libraries

Role Hierarchy

Level
Description

Organization Role

Default permissions for all libraries in the org

Library Role

Override permissions for a specific library

Example: A user with Org Editor role but Library Viewer role for "Engineering Library" will have:

  • Editor permissions in all other libraries

  • Viewer (read-only) permissions in Engineering Library

Legacy Role Mapping

If you were using the legacy OrganizationRoleEnum, here's how roles map:

Legacy Enum
New System Role
Permissions

SITE_ADMIN

Site Admin

Full system access (admin)

ADMIN

Admin

Full organization access (admin)

USER

Editor

Create, edit, approve change orders

APPROVER

Removed

Use Editor or Reviewer

REVIEWER

Viewer

Read-only access

SUPPLIER

Supplier

Limited external access

The USER role has been renamed to EDITOR to better reflect its capabilities. The APPROVER role has been removed - use EDITOR for users who need to approve change orders.

New RBAC Queries

Permission Categories

Resource
Actions

components

create, read, update, delete, revision.create

assemblies

create, read, update, delete

change_orders

create, read, update, delete, submit, approve, reject, release, withdraw, reset

library

read, settings.update, categories.manage, statuses.manage, features.manage

organization

read, settings.update, users.read, users.invite, users.remove, users.update_role

roles

read, create, update, delete, assign

comments

create, read, update, delete, moderate

Permission Errors

API v2 will return 403 Forbidden with detailed error codes when permission checks fail:

Permission checks run server-side — there is no public query to check permissions yourself. The "permission checks on every request" behavior means Duro evaluates the caller's effective role and enforces access automatically; it does not expose a client-callable permission-check query on the public API. The internal checkPermission operation is not part of the public federated schema. To determine access from your integration, query a user's assigned roles and permissions (see Role-Based Access Control) or handle the FORBIDDEN error above.


Invitation System Changes

Type Rename

The OrganizationInvitation type has been renamed to Invitation and enhanced with RBAC support:

Bulk Invite Changes


Dashboard queries: useMockData removed

The DashboardQueryOperations fields quickStats, userData, activityData, and analytics previously accepted an optional useMockData: Boolean argument. Passing true returned fabricated components, change orders, and user names instead of your library's real data — it backed the "Live Mode" toggle in the Duro web app.

That argument has been removed from the federated schema. These fields now always return real data for the library resolved from your x-library header.

Field
Before
After

dashboard.quickStats

quickStats(useMockData: Boolean)

quickStats

dashboard.userData

userData(useMockData: Boolean)

userData

dashboard.activityData

activityData(useMockData: Boolean)

activityData

dashboard.analytics

analytics(useMockData: Boolean)

analytics

A library with no data now returns empty results rather than sample content, so make sure your integration handles empty stats and activity lists.


API keys: hashedKey removed and management by id

The ApiKey type no longer exposes hashedKey, and a key's stored hash is no longer accepted anywhere a credential is expected. Previously the hash doubled as both a lookup handle (which key am I renaming?) and a credential (sending it as x-api-key authenticated). Those two jobs are now separate: the row id is the handle, and only the raw key you were shown at creation is a credential.

What changed

Surface
Before
After

ApiKey.hashedKey

Selectable

Removed. Selecting it fails validation with GRAPHQL_VALIDATION_FAILED.

UpdateApiKeyInput

apiKey: String! (raw key or hash)

id: String or apiKey: String (raw key only) — exactly one required

DeactivateApiKeyInput

apiKey: String! (raw key or hash)

id: String or apiKey: String (raw key only) — exactly one required

RotateApiKeyInput

apiKey: String! (raw key or hash)

id: String or apiKey: String (raw key only) — exactly one required

x-api-key header

Raw key or hash authenticated

Raw key only. A hash returns UNAUTHENTICATED.

apiKey query filter / validateApiKey

Matched raw key or hash

Raw key only

Selecting a key to manage

Every management call must identify exactly one key. Pass either the key's id (from apiKeys / apiKey list results) or the raw apiKey string. Supplying both, or neither, is rejected. Both selectors resolve only to keys owned by the calling user.

The examples above show the input shape. Confirm the exact mutation names for update, deactivate and rotate in the GraphQL Playground.

Migrating

  1. Remove hashedKey from every selection set, including generated clients and persisted documents. Any query that still selects it fails validation for the whole operation.

  2. Stop storing or forwarding hashes. If your integration read hashedKey from list results to drive rename, rotate or revoke, switch to id. The id is stable for the life of the key.

  3. Keep passing the raw key only if you already hold it. Callers that authenticate with a raw key and then rotate or deactivate that same key may keep using apiKey. Never pass a hash.

  4. Expect UNAUTHENTICATED on hashed credentials. Any stored hash that was previously being replayed as x-api-key stops working immediately. No key rotation is required; existing raw keys keep authenticating.


Component.documentFormats removed

The per-component list of generated document formats is gone. documentFormats: [String!] no longer exists on the Component type, nor on the CreateComponentInput, UpdateComponentInput, CreateItemInput and UpdateItemInput input types. The field recorded which derived files (PDF, STEP, …) a CAD plugin should generate for a part; that decision is no longer made per part, so there is no replacement field in the GraphQL API.

What changed

Surface
Before
After

Component.documentFormats

Selectable

Removed. Selecting it fails validation with GRAPHQL_VALIDATION_FAILED.

CreateComponentInput.documentFormats

Optional [String!]

Removed. Sending it is rejected as an unknown input field.

UpdateComponentInput.documentFormats

Optional [String!]; null cleared the list

Removed. Sending it is rejected as an unknown input field.

CreateItemInput.documentFormats

Optional [String!]

Removed. Sending it is rejected as an unknown input field.

UpdateItemInput.documentFormats

Optional [String!]; null cleared the list

Removed. Sending it is rejected as an unknown input field.

Migration steps

  1. Remove documentFormats from every selection set, including generated clients and persisted documents. A query that still selects it fails validation for the whole operation.

  2. Stop sending documentFormats in create and update inputs. Any object that still carries the key is rejected, so strip it before serializing rather than relying on the server to ignore it.

  3. Do not look for a replacement field. Where generated documents are saved is now a library-level integration setting managed by a library admin, not something the component API exposes or accepts. The documents a component actually holds are unchanged and still queried as before — see Documents.


Sandbox Organizations

Sandbox organizations are full organizations of type = SANDBOX that inherit their parent organization's plan and entitlements. They give admins a real org to experiment in — practice SSO setup, model libraries differently, run demos or training — without touching production. A sandbox behaves like any other organization (its own libraries, members, and features) but is created as a child of an existing real (non-sandbox) organization and does not consume an organization grant.

These fields are additive — they do not change any existing v2 behavior. Whether the API surfaces sandbox creation depends on your subscription's sandbox_orgs entitlement.

GraphQL surface

Deciding what to create

Query orgCreationOptions to learn whether the current user can create a production organization, a sandbox, or both, and which parent organizations still have sandbox headroom:

OrgCreationOptions and SandboxParentOption are new types; the selection above is representative. Use the GraphQL Playground as the source of truth for their exact fields.

Creating a sandbox

Pass parentOrganizationId when creating an organization to make it a sandbox child of that parent. The parent must be a real organization you are Site Admin of, and it must have remaining sandbox_orgs headroom — otherwise the request fails with SANDBOX_PARENT_FORBIDDEN (see Error Handling).

Archiving a sandbox

Archiving a sandbox frees its slot — only non-archived sandboxes count against the parent's sandbox_orgs quota. The mutation only archives sandbox organizations you administer; other targets are rejected with SANDBOX_ARCHIVE_FORBIDDEN.


Principal model (integration access)

Duro treats every writer of a component — people working in Duro, and each connected integration — as a principal with explicit component links and per-attribute access. Attributes are either open (writable by anyone with components.update) or managed (writable only by principals linked to the component). The model and its GraphQL surface are documented in full under Integration Access and Provenance.

These fields and mutations are additive — nothing in the existing v2 component contract was removed or renamed. Attribute management is opt-in per library, so an organization that has configured none behaves exactly as before.

What was added

Surface
Added

Component

effectiveAttributeAccess, viewerAccess, integrationLock, actingIntegrationId, originIntegrationId, originIntegration

Integration policy

recommendedAttributes, accessPolicy, setAttributeAccess

Component sync

reconcile, linkComponent, unlinkComponent

What integrations should change

  1. Handle the new write failures. A component write that touches a managed attribute can now fail with ATTRIBUTE_LOCKED_BY_INTEGRATION or ATTRIBUTE_REQUIRES_COMPONENT_LINK where it previously succeeded. This only happens once someone configures management for your integration's library, so add the handling before it bites.

  2. Read access before offering an edit. Query effectiveAttributeAccess and disable fields the acting principal cannot write, rather than discovering it from an error.

  3. Do not treat a missing attribute as unset. Reads made on behalf of an integration configured no-access for an attribute withhold that attribute instead of failing. Round-tripping a read into a write can otherwise erase values you were never shown.

  4. Link explicitly. Writing does not claim a component. If your integration needs to own managed attributes, call linkComponent; links are additive and do not displace other integrations.

  5. Check your null handling. attributeValues merges: an absent attribute is left alone, and a wrapped null deletes the value. Clients that strip nulls before serializing will turn intended deletes into no-ops.


Efficient Migration Strategy

Step 1: Update HTTP Client Configuration

Configure your HTTP client to automatically include context headers:

Step 2: Search and Replace Input Objects

Use these regex patterns to find affected code:

Step 3: Update Query/Mutation Calls

  1. Remove libraryId parameters from all input objects

  2. Remove library and libraryId arguments from query/mutation calls

  3. Ensure headers are set before making requests

Step 4: Handle New Error Responses

Update error handling to recognize permission errors:

Step 5: Test Thoroughly

  1. Test all CRUD operations for components

  2. Test change order workflows

  3. Test configuration updates

  4. Verify webhook functionality

  5. Test with different user roles


FAQ

Q: Can I use both v1 and v2 patterns?

A: No. The v2 schema removes the parameters entirely. You must update all calls.

Q: What if I access multiple libraries?

A: Change the x-library header for each request. Most HTTP clients support per-request header overrides.

Q: How do I get my organization/library slugs?

A: View your URL in Duro: durohub.com/org/@org-slug/libs/library-slug. The slugs are the @org-slug and library-slug portions.

Q: My API key was created for a specific library. Do I need a new one?

A: No, existing API keys continue to work. Just ensure your x-library header matches the library your key was created for.

Q: What happens if headers are missing?

A: You'll receive a 400 Bad Request error indicating which headers are required.


Support

If you encounter issues during migration:

  1. Check the Error Handling guide

  2. Review the GraphQL Playground at https://api.durohub.com/graphql

  3. Contact support at support@durolabs.co


Next Steps

  • Explore the GraphQL Playground with the new schema

  • Update your Webhooks configuration if needed

  • Review the complete permission list in your organization's Settings > Roles & Permissions

Last updated

Was this helpful?