API Changelog & Migration
This page tracks breaking changes, deprecations, and additions across the Molecule APIs. Use it when upgrading an existing integration. Each API's most recent changes are listed first.
Authentication
The service-token sign-in message is now single-use and expires
getServiceSignInMessage used to return a deterministic string β a pure function of (walletAddress, serviceName) β which meant one captured signature could mint fresh tokens indefinitely. The message now embeds a server-issued single-use nonce and its expiry, and generateServiceToken verifies and consumes that nonce:
The message is valid for 10 minutes from issuance. The new
expiresAtfield ongetServiceSignInMessagereports the deadline.The nonce is consumed on first successful redemption. Issuing a second token requires a fresh message and a fresh signature.
There is one outstanding nonce per
(walletAddress, serviceName), last-write-wins β calling the query again invalidates an unredeemed message.Signatures over the older nonce-free message format no longer verify.
Failures come back as UNAUTHENTICATED with details.reason one of NONCE_NOT_FOUND (never requested, or already consumed), NONCE_EXPIRED, or INVALID_SIGNATURE (altered text, a message superseded by a later call, or a walletAddress that is not the signer).
Migration: Fetch the message immediately before signing, and treat every one of those reasons as "request a new message and sign it again" rather than as a retryable call β re-submitting the same signature can never succeed. Callers that already ran getServiceSignInMessage β sign β generateServiceToken back to back need no change; callers that cached the message, cached a signature, or reconstructed the string client-side must stop doing so. See Obtaining a Token.
Service tokens are self-issued, and scoped to their own lifecycle
Two clarifications and one hardening, all now reflected across the API docs:
Nobody provisions a service token for you.
generateServiceTokenaccepts a wallet signature, so any caller mints its own:getServiceSignInMessageβ sign the message verbatim (EIP-191personal_sign) βgenerateServiceToken. The only credential that still comes from the Molecule team is themol_consumer credential. Earlier pages described service tokens as team-issued; that was never the only path and is no longer the documented one.A service token is bound to a wallet, not to a lab. Docs that described it as identifying "which lab you have write access to" were wrong. It carries a wallet identity; what it may do on a given lab is resolved per request from that wallet's live onchain role. One token therefore works across every lab the wallet has a role on, and a role granted after issuance takes effect without re-issuing.
extendServiceTokenandrevokeServiceTokenare scoped to the caller's own tokens. The presented token must own thetokenIdit names; atokenIdbelonging to another wallet returns the byte-identicalNOT_FOUNDof one that does not exist, so token existence cannot be enumerated.
Migration: None required if you already self-issue. If you hold a team-provisioned token, it keeps working β but you can mint and rotate your own. If you built per-lab token issuance, you can collapse it to one token per wallet. If any code called extendServiceToken / revokeServiceToken for a tokenId issued to a different wallet, it now receives NOT_FOUND. See Authentication and Service Tokens.
Contributor role parity for service-token content writes
The six content-write mutations β initiateCreateOrUpdateFile, finishCreateOrUpdateFile, deleteDataRoomFile, updateFileMetadata, moveEntry and createAnnouncement (announcements have since been deprecated) β now gate a service token on the Contributor role, matching the Privy user path per mutation. Previously the service path required lab ownership for these, which meant a wallet granted Contributor on a lab could act through a user session but not through its own service token.
This is what unblocks the "human owns the lab, agent contributes to it" flow: the owner grants the agent's wallet Contributor, and the agent's self-issued token can write. Owner-gated surfaces are unchanged β createLab, updateLabNftMetadata and generateLabImageUploadUrl still require ownership.
Migration: None β this is a widening. Note that role state reaches the API through an event indexer, so a write can still return UNAUTHORIZED for a few seconds after a grant confirms onchain; retry with backoff rather than re-issuing the token. Walkthrough: Agent access.
Backend credential stores confined to the platform network
The data stores behind API authentication β the consumer credential registry, machine service tokens, and the access whitelist β are now network-confined to Molecule's private cloud network. They are unreachable from outside it, even with valid cloud-account credentials; only the API's own backend can read or write them. This is a hardening change with no effect on any API, header, token format, or SDK β consumer credentials, X-Service-Token, and Privy user tokens all work exactly as before.
Migration: None required.
x-api-key replaced by consumer credentials
All Molecule APIs (Labs, Tokenization β they share one GraphQL endpoint) now authenticate with a consumer credential instead of an x-api-key header. A consumer credential has the shape mol_<consumerId>_<secret> and is sent directly as the Authorization header value, with no Bearer prefix.
Migration: Request a consumer credential (see Getting Started) and send it as Authorization: mol_<consumerId>_<secret> instead of x-api-key β do not prefix it with Bearer, which is reserved for Privy user tokens and will fail authentication. Nothing else changes: X-Service-Token for machine-authorized mutations, and Authorization: Bearer <Privy token> + x-wallet-address for user-authorized mutations, work exactly as before. See Authentication for the full header reference.
Labs API
Lists without paging arguments now fail above a size limit
Nine list fields return every item and take no first / limit argument: Lab.members, LabRef.members, ListLabMembersResult.members, DataRoom.files, Announcement.attachments, SearchLabsAnnouncement.attachments, OnChainEvent.events, FileCategoriesAndTagsResult.data and FileCategory.tags. Each now has a limit of 1000 items, stated in its field description. At or under the limit nothing changes. Above it, a query that selects the list fails with COMPLEXITY_LIMIT_EXCEEDED, details.reason: "RESULT_CARDINALITY_LIMIT", plus details.field (for example DataRoom.files) and details.limit. A shortened list is never returned as if it were complete.
What fails depends on the list. Lab.members and LabRef.members fail on their own: that lab's members is null with the error at its path, and the rest of the response loads. Every other list fails the root field that returned it: one onChainActivity transaction over the events limit fails the page. A query that does not select the list is not affected. Limits sit well above anything production serves today. Token.relations and Token.markets have worked this way, with a limit of 50, since they were introduced.
Migration: No action required for current data. Treat the error as non-retryable: drop the list from the selection (an onChainActivity page without events always loads, and its last id is the cursor past the transaction), or narrow the query (a smaller page, a single lab). If you expect to exceed a limit, contact the Molecule team to raise it.
Announcements are deprecated
Announcements are no longer surfaced in the Molecule app, and they are out of every tutorial, how-to and feature-description page. The API surface is unchanged and still works β nothing has been removed from the schema and no call has started failing. This is a "stop building on it" notice, not a breaking change.
Still live, and still returned to callers who ask for it:
createAnnouncement mutation
Live. Gated on Contributor, like the file writes
/x402/labs/createAnnouncement gateway endpoint
Live, still in X402_WRITE_MUTATIONS, still priced
LabEventAnnouncement in the LabActivityNode union
Live β returned by unfiltered labActivity / activities
SearchLabsAnnouncementHit in the SearchLabsHit union
Live β returned by searchLabs
LabActivityFilter.ANNOUNCEMENT
Live
Migration: None required; existing integrations keep working. Do not add new dependencies on announcements. If you consume labActivity or activities and want a file-only feed, pass filter: FILE rather than assuming one β the unfiltered feed still contains announcement nodes for labs that have them. If you switch on __typename across LabActivityNode or SearchLabsHit, keep the announcement arms handled. See Browse & Search.
Assignment Agreement is no longer a gate, and is out of the API docs
Signing the assignment agreement is not a precondition for createLab, for uploading files, or for any other Labs API operation. It was previously presented as a required onboarding step, and the <AGREEMENT_TYPE>_NOT_SIGNED / AGREEMENT_CHECK_UNAVAILABLE failure causes are now dormant β reserved in the catalogue, but not emitted.
The legalAgreement* operations remain in the schema and are unchanged, but they have been removed from every onboarding and reference flow, and Legal Agreements is out of the site navigation. Do not build a new integration around them.
Migration: Delete any agreement-signing step from your workflow β it does nothing. If you branch on an agreement status before writing, remove the branch. Nothing in the API changed; only what is required of you did. The onboarding tutorials no longer include the step: Tutorials.
Staging introspection is the supported way to get the schema
Production has introspection disabled (below), but staging has it enabled β point codegen, a playground or an SDK generator at https://staging.graphql.api.molecule.xyz/graphql with your consumer credential and generate normally. Both environments serve the same schema, so generate against staging and point the generated client at production.
This supersedes the earlier guidance to request a copy of the schema from the Molecule team.
Migration: If your codegen currently fails against production, repoint it at staging. See Getting the schema.
GraphQL introspection disabled and query depth capped in production
The production endpoint (shared by all Molecule APIs β see API Overview) no longer serves __schema / __type introspection queries: they now return a validation error. __typename still resolves. Selection-set depth is also capped at 10 in production, with scalar leaves counted as a level ({ root { child { name } } } is depth 3). A query beyond that limit fails at execution time with errorType: "QueryDepthLimitReached" and partial data β a plain GraphQL error, not the catalogued error shape used elsewhere, so handle both.
Migration: If your codegen or tooling discovers the schema by introspecting the production endpoint, that now fails β introspect staging instead, where it is enabled, and point the generated client at production (see Getting the schema). If you see QueryDepthLimitReached, flatten the query to 10 levels of nesting or fewer; this limit was not previously enforced.
isSuccess removed (unified error contract)
The Labs API now reports failure one way per operation class, and the isSuccess envelope is gone:
Queries throw. A failed query surfaces as an entry in the top-level GraphQL
errors[]array; the failed field isnull, and because most Labs query fields are non-null the null propagates anddataitself comes backnull(onlylabWithDataRoomAndFilesanddataRoomFilenull just their own field).errorTypecarries the error code (table below) β the only value to branch on β anderrorInfo { requestId, retryable, details }carries the correlation id and structured context. Query result types (ActivitiesResult,FileCategoriesAndTagsResult,ListLabMembersResult,DidLinkStatusResult,LegalAgreementTemplateResult,ServiceSignInMessageResult) no longer have anisSuccessor anerrorfield.Mutations return errors in-band. Every mutation
*Resultcarries a nullableerror: ApiError. Success βerror == null. A top-levelerrors[]entry on a mutation means a transport or infrastructure failure (or an invalid request document), not a business outcome. Where a result keeps a top-levelmessage, it mirrorserror.messageon failure and is never empty.
isSuccess has been removed from every Labs result type, and the error field on mutation results is now typed ApiError (the previous error type no longer exists). A document that still selects isSuccess is rejected at validation (Field 'isSuccess' in type 'β¦Result' is undefined) and never executes.
Behaviour that changed alongside the envelope:
Auth denials are code-accurate. The former
AUTH_FAILEDcatch-all is split intoUNAUTHENTICATED(missing, invalid or expired credentials),UNAUTHORIZED(authenticated but lacking the role or membership),NOT_FOUND(the lab does not exist) andVALIDATION_FAILED(malformed input).Legacy codes live on as
details.reason. Codes such asPROJECT_NOT_FOUND,LAB_NOT_FOUND,INVALID_OCL_IDorSHORTNAME_TAKENare no longer top-level codes; they survive as thereasonkey insidedetails, beneath the catalogue code (for exampleCONFLICTwithreason: "SHORTNAME_TAKEN"). Branch oncodefirst and onreasononly where a page documents it.Service tokens.
ServiceTokenResultandServiceTokenRevocationResultgainederror: ApiError;token,tokenId,serviceName,expiresAt,createdAtandrevokedAtarenullon failure instead of"".InitiateFileUploadResult.methodis nullable andnullon failure.LegalAgreementStatusResultis dual-surface. As thelegalAgreementStatus(oclId, type)query, failures throw anderroris alwaysnull. As thelegalAgreementStatusfield onLab/LabRef, an upstream failure returns the payload witherrorset (typicallyUPSTREAM_UNAVAILABLE) instead of nulling the lab:error != nullmeans the status could not be determined, and only whenerror == nulldoessigned: falsemean "not signed".listLabMemberson an unknown lab throwsNOT_FOUND(previously aPROJECT_NOT_FOUNDenvelope).createLabconflicts returnCONFLICTwithdetails.reasonPROJECT_CONFLICT(lab already registered) orACCOUNT_NAME_CONFLICT.
The Tokenization API is unaffected. Its operations keep their previous result envelope (a success flag alongside an
errorobject), exactly as documented in the Tokenization API reference. This section applies to the Labs API only.
ApiError
On an in-band mutation error, details arrives as a JSON-encoded string (AppSync AWSJSON), e.g. "details": "{\"reason\":\"NOT_LAB_OWNER\"}"; on a thrown query error, errorInfo.details is a plain object. The in-band string is currently encoded twice, so read it with the tolerant parseDetails rather than a single JSON.parse, which returns another string and makes .reason silently undefined. Ignore keys you do not recognise, and never match on message β its wording may change without notice.
Error codes
Code
retryable
Meaning
Typical details.reason
UNAUTHENTICATED
false
Missing, invalid or expired credentials.
AUTH_FAILED, SERVICE_AUTH_FAILED, INVALID_SIGNATURE, NONCE_NOT_FOUND, NONCE_EXPIRED, WALLET_MISMATCH, CONSUMER_CREDENTIAL_REQUIRED
UNAUTHORIZED
false
Authenticated but not allowed (role or membership).
UNAUTHORIZED (role/membership denial), NOT_LAB_OWNER, MUTATION_NOT_ALLOWED
NOT_FOUND
false
The referenced resource does not exist.
LAB_NOT_FOUND, OCL_NOT_FOUND, TOKEN_NOT_FOUND, PROJECT_NOT_FOUND, β¦
VALIDATION_FAILED
false
Input failed validation; details.field names the offending field.
INVALID_OCL_ID, INVALID_INPUT, MISSING_INPUT, β¦
CONFLICT
false
A valid request conflicts with current state.
SHORTNAME_TAKEN, PROJECT_CONFLICT, ACCOUNT_NAME_CONFLICT, ALREADY_REVOKED
FAILED_PRECONDITION
false
Resource state makes the operation impossible until that state changes.
TOKEN_REVOKED, LEGACY_ENCRYPTION, NOT_ENCRYPTED, MISSING_DEK
COMPLEXITY_LIMIT_EXCEEDED
false
Query shape or result size is over the limit.
COMPLEXITY_LIMIT_EXCEEDED, RESULT_CARDINALITY_LIMIT (list limits)
RATE_LIMITED
true
Throttled β retry with backoff.
β
TIMEOUT
true
Execution exceeded the request budget.
β
UPSTREAM_UNAVAILABLE
true
A dependency failed β retry with backoff.
KAMU, CMS, IPFS
INTERNAL_ERROR
true
Unexpected failure; quote requestId when reporting it.
TOKEN_GENERATION_FAILED, CREATE_LAB_FAILED, UPLOAD_INIT_ERROR, KMS_ERROR, β¦
Codes may be added over time, and each addition is published on this page. Treat a code you do not recognise as non-retryable, keep the raw value for diagnostics and surface it to a human. details.reason values are diagnostic refinement, not a contract surface β they may be extended without notice. For errors outside the GraphQL API, such as x402 Gateway responses, see Errors.
Before / after
Migration: Remove isSuccess (and, except on legalAgreementStatus, any error { β¦ } selection) from every Labs query document β a document that still selects it fails validation and the query never runs β and handle failures from the top-level errors[] array, keyed on errorType. On mutations, replace isSuccess with error { code message requestId retryable details }, treat error == null as success, and branch on error.code (plus details.reason where documented), never on message text. Replace any check on the former AUTH_FAILED catch-all with the split codes listed above, and any token === "" / tokenId === "" checks on service-token results with null checks. If you read legalAgreementStatus off a lab object, select error { code message } and check it before trusting signed. Leave your Tokenization API handling as it is. See Errors for the full reference.
*V2 operations and pre-OCL naming removed
The legacy *V2 operations and the pre-OCL naming have been removed. The current API is oclId-based. If you are migrating from an older integration, use the current names below.
Renamed queries
projects / projectsV2
labs
Same paginated shape; adds an optional role filter
projectWithDataRoomAndFiles / β¦V2
labWithDataRoomAndFiles
Look up by oclId (or shortname) instead of ipnftUid
dataRoomFileV2
dataRoomFile
Identified by oclId + path
projectActivity / projectActivityV2
labActivity
β
projectAnnouncementsV2 / projectAnnouncementV2
labActivity / activities
Use the filter: ANNOUNCEMENT argument (announcements since deprecated)
activitiesV2
activities
β
Renamed mutations
createProject
createLab
Now takes input: { oclId } instead of ipnftSymbol / ipnftTokenId
createAnnouncementV2
createAnnouncement
Takes oclId; the legacy moleculeAccessLevel param was removed. Announcements since deprecated
initiateCreateOrUpdateFileV2
initiateCreateOrUpdateFile
β
finishCreateOrUpdateFileV2
finishCreateOrUpdateFile
β
updateFileMetadataV2
updateFileMetadata
β
deleteDataRoomFileV2
deleteDataRoomFile
β
Renamed fields
Top-level identifiers on Lab / LabRef were renamed away from the legacy IP-NFT naming:
ipnftUid
oclId
Now a 32-byte hex string (0xβ¦), not <address>_<tokenId>
ipnftSymbol
shortname
Human-readable slug derived from the lab name
ipnftAddress
labAccountAddress
ERC-6551 token-bound account address
ipnftTokenId
labNftTokenId
LabNFT tokenId (decimal string)
The linked legacy IP-NFT (for labs migrated from one) is still available as the nested
ipnftobject onLab/LabRef;LabRefadditionally exposes a scalaripnftIdfield.
x402 Gateway
Gateway base URLs published, and the 402 challenge is a header
The staging and production gateway base URLs are now published on the x402 Gateway page β they no longer have to be requested.
Along with them, one correction that matters for anyone implementing the handshake: the payment requirements arrive as base64-encoded JSON in the payment-required response header, not in the 402 response body. The body is only {"isSuccess":false,"message":"Payment required"}. Client code that parsed the body for accepts never saw a price.
Migration: Read the challenge from the payment-required header and base64-decode it; take amount, asset, network and payTo from accepts[0]. amount is in the asset's smallest unit (USDC has 6 decimals, so "10000" is $0.01). Worked example: Reading the 402 challenge.
Also worth knowing: payment buys a short-lived service token for the payer wallet, not a role. A mutation the payer is not authorized for returns 200 with error.code: "UNAUTHORIZED" and is still settled β check the target lab and your role on it (both free, public queries) before signing.
February 2026
Breaking Changes
Schema Flattening β metadata wrapper removed from IPNFT and IPT
The intermediate metadata wrapper objects (IPNFTMetadata, IPTMetadata) have been removed. All metadata fields now live directly on the IPNFT and IPT types.
Migration: Remove all metadata { ... } wrappers and access fields directly on the parent type.
fundingAmount JSON field decomposed into 4 typed fields
The fundingAmount JSON field on IPNFT has been replaced with 4 strongly-typed fields:
Migration example:
agreements changed from JSON array to typed relation
The agreements field on IPNFT has changed from a JSON array to a queryable typed relation with sub-field selection:
Migration: Update your queries to select specific agreement fields instead of receiving a raw JSON array.
Filter changes β nested metadata filters removed
All filter paths that previously went through metadata are now direct fields:
researchLead and originalOwner filter paths changed
These relation filters are now direct on the parent type instead of nested inside metadata context:
New Features
New query types
The following new top-level queries are now available:
user(id) / users(...)
Query users by address, list associated IP-NFTs and IPTs
researchLead(id) / researchLeads(...)
Query research leads and their associated IP-NFTs
chain(id) / chains(...)
Query blockchain networks and their markets
agreement(id) / agreements(...)
Query legal agreements associated with IP-NFTs
All new queries support limit, skip, sortBy, sortOrder, and filterBy parameters.
New fields on IPNFT
updatedAtβ Last update timestamptokenUriβ Token metadata URIsymbolβ Token symbolschemaVersionβ Metadata schema versionuserIdβ Owner user ID (for direct filtering)researchLeadIdβ Research lead ID (for direct filtering)fundingAmountCurrency,fundingAmountValue,fundingAmountDecimals,fundingAmountCurrencyTypeβ Decomposed funding amount fields
New fields on IPT
updatedAtβ Last update timestampmintedAtβ Minting timestampagreementMimeTypeβ Agreement file MIME typeoriginalOwnerβ FullUsertype (withid,address)originalOwnerIdβ Original owner user ID (for direct filtering)ipnftIdβ Parent IP-NFT ID (for direct filtering)linksβ Related linkscappedβ Whether token issuance is capped
New fields on Market
createdAt,updatedAtβ Timestampsinvertedβ Whether the pair is invertediptIdβ Associated IPT ID (for direct filtering)chain.logoUrlβ Chain logo URL now available
agreements queryable as typed relation
Agreements on IP-NFTs are now a fully queryable relation with sub-field selection, filtering, sorting, and pagination:
Related
Release Notes β per-version release notes
Labs API β the current Labs API reference
API Types β every type as it stands today
Last updated
Was this helpful?