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
libraryIdas a direct parameterOthers expected it within an
inputobjectSome used
library(slug) while others usedlibraryId(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?
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
Consistency: Every operation uses the same context mechanism
Cleaner Queries: No redundant library/organization IDs in every request
Fine-grained Permissions: New RBAC system enables granular access control
Better Security: Permission checks on every request
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-446655440000Organization slug:
@my-company
x-library (Required for library-scoped operations)
Identifies the library context. Accepts either:
Library UUID:
660e8400-e29b-41d4-a716-446655440000Library 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.
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.
2. Input Objects: Modified Fields
Fields changed or deprecated in input types.
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.
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
4. Root Mutation Changes
Changes to top-level Mutation fields.
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
findAll
findAll(libraryId: ID!, filter: CategoryFilterInput)
findAll(filter: CategoryFilterInput)
ChangeOrdersQueryOperations
get
get(library: String, filter: ChangeOrderFilterInput, pagination: PaginationInput)
get(filter: ChangeOrderFilterInput, pagination: PaginationInput)
getTemplates
getTemplates(library: String)
getTemplates
ComponentQueryNamespace
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
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
invitations
invitations(filter: InvitationFilterInput): [OrganizationInvitation!]!
invitations(filter: InvitationFilterInput): [Invitation!]!
discoverByDomain
(not present)
discoverByDomain: Organization
WebhookQueryNamespace
findAll
findAll(input: FindWebhooksInput!)
findAll
6. Namespace Mutation Method Changes
Changes to methods within mutation namespaces.
ChangeOrdersMutationOperations
create
create(input: CreateChangeOrderInput!, libraryId: ID)
create(input: CreateChangeOrderInput!)
createTemplate
createTemplate(input: CreateTemplateInput!, library: String)
createTemplate(input: CreateTemplateInput!)
ConfigMutationNamespace
manuallyProcessCategoriesConfig
manuallyProcessCategoriesConfig(libraryId: String!)
manuallyProcessCategoriesConfig
OrganizationMutationNamespace
membership
membership(organizationId: String!): MembershipMutationNamespace!
membership: MembershipMutationNamespace!
declineInvitation
(not present)
declineInvitation(id: String!): Boolean!
MembershipMutationNamespace
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
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
OrganizationInvitation
Invitation
Enhanced with RBAC fields
Modified Types
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)
AltiumHealthCheckResponse
Altium integration
ViewerUrlResponse
Altium integration
GenerateViewerUrlInput
Altium integration
OrganizationInvitation
Renamed to Invitation
FindWebhooksInput
Webhooks (no longer needed)
10. New Types (Complete List)
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
Permissions: Granular capabilities like
components.create,change_orders.approveRoles: Collections of permissions (e.g., Editor, Viewer, Admin)
Role Types: Organization-level (
ORG) or Library-level (LIBRARY)Hierarchical Resolution: Library roles override organization roles for specific libraries
Role Hierarchy
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:
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
New RBAC Queries
Permission Categories
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:
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.
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
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.
Migrating
Remove
hashedKeyfrom every selection set, including generated clients and persisted documents. Any query that still selects it fails validation for the whole operation.Stop storing or forwarding hashes. If your integration read
hashedKeyfrom list results to drive rename, rotate or revoke, switch toid. Theidis stable for the life of the key.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.Expect
UNAUTHENTICATEDon hashed credentials. Any stored hash that was previously being replayed asx-api-keystops 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
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
Remove
documentFormatsfrom every selection set, including generated clients and persisted documents. A query that still selects it fails validation for the whole operation.Stop sending
documentFormatsin 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.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.
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:
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.
What was added
Component
effectiveAttributeAccess, viewerAccess, integrationLock, actingIntegrationId, originIntegrationId, originIntegration
Integration policy
recommendedAttributes, accessPolicy, setAttributeAccess
Component sync
reconcile, linkComponent, unlinkComponent
Errors
What integrations should change
Handle the new write failures. A component write that touches a managed attribute can now fail with
ATTRIBUTE_LOCKED_BY_INTEGRATIONorATTRIBUTE_REQUIRES_COMPONENT_LINKwhere it previously succeeded. This only happens once someone configures management for your integration's library, so add the handling before it bites.Read access before offering an edit. Query
effectiveAttributeAccessand disable fields the acting principal cannot write, rather than discovering it from an error.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.
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.Check your
nullhandling.attributeValuesmerges: an absent attribute is left alone, and a wrappednulldeletes the value. Clients that stripnulls 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
Remove
libraryIdparameters from all input objectsRemove
libraryandlibraryIdarguments from query/mutation callsEnsure headers are set before making requests
Step 4: Handle New Error Responses
Update error handling to recognize permission errors:
Step 5: Test Thoroughly
Test all CRUD operations for components
Test change order workflows
Test configuration updates
Verify webhook functionality
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:
Check the Error Handling guide
Review the GraphQL Playground at
https://api.durohub.com/graphqlContact 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?