🚨Errors
How the Labs API, the Tokenization API and the x402 Gateway report failure: where the error appears, what to branch on, and when to retry.
The Molecule APIs don't share one error shape. The Labs API uses ApiError, the Tokenization API keeps its older isSuccess + EvmTokenizationError envelope, and the x402 Gateway adds an HTTP layer in front of Labs mutations. This page covers all three, plus the rules that apply to every one of them.
Errors that only one operation produces stay on that operation's page, for example the sign-in failure reasons or the tokenization contract reverts. This page links to them.
Which contract applies
A Labs API query
top-level errors[]; the field (often data itself) is null
errors[i].errorType
errors[i].errorInfo.retryable
errors[i].errorInfo.requestId
A Labs API mutation
the result's error field (ApiError), HTTP 200
error.code
error.retryable
error.requestId
A Tokenization API query or mutation
the result's isSuccess: false and error field (EvmTokenizationError), HTTP 200
error.code
error.retryable
none in-band; errorInfo.requestId on the few thrown errors
A mutation through the x402 Gateway
the HTTP status first; on 200, the Labs mutation contract above
HTTP status, then error.code
none; on settlement, the transaction hash
Whichever API you call, the message is for humans and may change without notice. Branch on the code or status, never on the message text.
Failures before your operation runs
Some requests fail before any resolver sees them. They look the same whichever API you called.
Missing or malformed consumer credential: HTTP 401. The Authorization header is checked before the GraphQL layer runs, so there's no errorType and no ApiError. The mol_… credential goes in directly, with no Bearer prefix. See Authentication.
Malformed request body: HTTP 400. A body that isn't valid JSON is rejected before it's parsed as GraphQL, and before the credential is checked. The body has one errors[] entry with errorType: "MalformedHttpRequestException" and no errorInfo.
Blocked at the edge: HTTP 403. The API sits behind a web application firewall with request-count rules. A request it blocks gets a raw 403 with a non-GraphQL body: no errors[], no errorType, no errorInfo. These rules only count requests today and block nothing. If they start blocking, slow down before retrying. This is not the RATE_LIMITED error, which is a catalogued GraphQL error.
Invalid GraphQL document: a plain GraphQL error with no code. A query that selects a field that doesn't exist, or passes a variable of the wrong type, returns a top-level errors[] entry. It has a message but no catalogued errorType and no errorInfo. Fix the document; retrying it unchanged won't help. On a mutation, an errors[] entry like this means the document failed validation and the mutation didn't run.
Query too deep: QueryDepthLimitReached. Production enforces a query-depth limit of 10. A query deeper than that fails at execution time with errorType: "QueryDepthLimitReached" and partial data. It's a plain GraphQL error, not the catalogued shape.
Your client has to handle both shapes: catalogued errors with errorInfo, and plain GraphQL errors without it.
Rate-limited requests
A credential that has spent its rate-limit budget for the current window gets RATE_LIMITED until the window ends. Rate limits aren't enforced yet; budgets are measured, but nothing is refused. How requests are priced, the budgets per credential, and the other limits are on Rate & Query Limits.
A denied request is refused before any resolver runs, so nothing was read or changed. It's raised the same way on every API and operation class, mutations and Tokenization API calls included: HTTP 200, a top-level errors[] entry and a null field, never an in-band error:
details key
Type
Meaning
reason
string
COST_BUDGET_EXHAUSTED
retryAfterSeconds
integer ≥ 0
Seconds until the current window ends. 0 means retry once now, then back off
remainingBudget
integer ≥ 0
Cost units left in the window. Currently always 0 on a denial
budgetWindowSeconds
integer > 0
Length of the window
Wait retryAfterSeconds, plus a little jitter, then retry the same request once. If it's denied again, back off exponentially, capped at budgetWindowSeconds. Never tight-loop: every retry is charged too.
Labs API errors
The Labs API is a GraphQL API: once a request is accepted, the response is HTTP 200 whether or not the operation succeeded. Success and failure are signalled inside the JSON body, not by the status code. Errors surface through one of two channels, depending on the operation class:
Queries throw. A failed query adds an entry to the top-level GraphQL
errors[]array and returnsnullfor that field. Most Labs query result types are non-null, so the null propagates anddataitself comes backnull(as in the example below); onlylabWithDataRoomAndFilesanddataRoomFileare nullable and null just their own field.errorTypecarries the error code (the only value to branch on) anderrorInfocarries{ requestId, retryable, details }.Mutations return errors in-band. Every mutation result type carries an
error: ApiErrorfield. Success ⇔error == null. Where the result type also has a top-levelmessage, it mirrorserror.messageon failure and is never empty. A top-levelerrors[]entry on a mutation means a transport or infrastructure failure, that the request document itself failed validation, or aRATE_LIMITEDdenial. In all three cases the mutation didn't run.
Include requestId whenever you report a problem.
Failed query
Failed mutation
Select error { code message requestId retryable details } on every mutation:
Reading details
details reaches you in more than one shape, so read it through a tolerant parse rather than a single JSON.parse: it is a JSON-encoded string on in-band mutation errors, a plain object on thrown query errors (errorInfo.details), and the in-band string is currently encoded twice — a single parse there returns another string, and .reason on it is silently undefined. Parsing until the value stops being a string reads all three correctly and needs no change when the encoding is corrected.
Documented keys are field (the offending input field), reason (a more specific cause under the code, e.g. PROJECT_NOT_FOUND under NOT_FOUND), hint and docs; ignore unknown keys. reason values are diagnostic refinement and may be extended at any time — branch on code first.
A few codes carry extra keys that are part of the contract:
Code and reason
Extra keys
RATE_LIMITED / COST_BUDGET_EXHAUSTED
retryAfterSeconds, remainingBudget, budgetWindowSeconds. See Rate-limited requests
COMPLEXITY_LIMIT_EXCEEDED / RESULT_CARDINALITY_LIMIT
field (the list, as Type.field) and limit (the largest list it serves). See list limits
UNAUTHORIZED / INTERNAL_FIELD
fields: the fields, as Type.field, that aren't part of the public API. See Labs troubleshooting
Keys that only one operation emits are documented on that operation's page.
Labs error codes
Code
retryable
Meaning
UNAUTHENTICATED
false
Missing, invalid or expired credentials
UNAUTHORIZED
false
Authenticated, but not allowed (role/membership)
NOT_FOUND
false
Referenced resource doesn't exist
VALIDATION_FAILED
false
Input failed validation (unknown filter/sort fields, out-of-range pagination, malformed ids); details.field names the offender
CONFLICT
false
Valid request conflicts with current state (e.g. details.reason SHORTNAME_TAKEN, ALREADY_SIGNED)
FAILED_PRECONDITION
false
Resource state makes the operation impossible until the state changes (e.g. TEMPLATE_EXPIRED)
COMPLEXITY_LIMIT_EXCEEDED
false
Query shape or result size over limits
RATE_LIMITED
true
Throttled — retry with backoff
TIMEOUT
true
Execution exceeded the request budget
UPSTREAM_UNAVAILABLE
true
A dependency failed (details.reason KAMU, CMS, IPFS)
INTERNAL_ERROR
true
Unexpected failure — details are only in our logs, joined by requestId
The typical details.reason values under each code are listed in the API Changelog. Codes may be added over time; see Unknown codes.
Labs troubleshooting
UNAUTHENTICATED — missing, invalid or expired service token:
Ensure the
X-Service-Tokenheader is included in mutation requestsVerify the token is not empty or malformed
If the token has expired, issue a new one yourself — the two-call sign-in flow needs no human — or extend the existing one with
extendServiceTokenThe sign-in path's own failure reasons are listed on Service Tokens
An HTTP 401 instead of UNAUTHENTICATED means the consumer credential was rejected, not the service token — see Failures before your operation runs.
UNAUTHORIZED — the wallet behind the service token lacks the required role on the lab:
Check the wallet's role with the public
listLabMembers(oclId)query. Content writes (uploads, metadata, moves, deletes) need Contributor;createLaband the LabNFT-metadata mutations need OwnerNot the right role? The lab owner grants one onchain — see Agent access
Just granted the role? Role state reaches the API through an event indexer, so a write can still return
UNAUTHORIZEDfor a few seconds after the grant confirms onchain. Retry with backoff; re-issuing the token does not helpdetails.reasonisINTERNAL_FIELD? The request selects, filters or sorts by a field that isn't part of the public API, anddetails.fieldslists them. Nothing was read. Remove those fields from the query; if your integration needs one, ask Molecule for access. This check only logs today and refuses nothing; enforcement will be announced in the API Changelog
NOT_FOUND — lab, dataroom or file not found:
Verify the
oclIdrefers to a registered labFor
updateFileMetadata/deleteDataRoomFile: verify the fileref(DID) orpathis correct and the file exists in the specified dataroom
VALIDATION_FAILED — invalid parameters; details.field names the offending input:
Check that the
oclIdformat is correct: a 32-byte hex string with0xprefixFor
searchLabs: verify filter values match expected types (arrays of strings)
Retryable errors (RATE_LIMITED, TIMEOUT, UPSTREAM_UNAVAILABLE, INTERNAL_ERROR):
Retry with exponential backoff; if the failure persists, report it with the
requestIdOne known exception: a bad
expiresInongenerateServiceTokencomes back asINTERNAL_ERROR/TOKEN_GENERATION_FAILEDand never succeeds on retry — see Service Tokens
Tokenization API errors
The Tokenization API keeps its own envelope. It did not move to ApiError, so handle it separately from the Labs API. Every result, queries included, carries a success flag and an error object, and returns HTTP 200 either way:
How it differs from the Labs API:
Success check
error == null
isSuccess == true (and error == null)
Queries
throw into errors[]
return isSuccess: false + error in-band, like mutations
code
always set (String!)
nullable (String); treat a missing code as non-retryable
requestId
on every error
not present
Select error { code message retryable } on every tokenization operation, not just error { message }.
Four failures bypass the envelope and arrive as catalogued, thrown GraphQL errors (top-level errors[], errorType, with errorInfo.requestId). Handle them the same way as a Labs query error:
COMPLEXITY_LIMIT_EXCEEDED: the selection set on a tokenization field is too deep or selects too many fields. Trim it. Not retryable.RATE_LIMITED: the credential's cost budget is spent. The operation didn't run. See Rate-limited requests. Retryable.TIMEOUT: the resolver ran out of time and was stopped. Retryable. On a mutation, the work may have partly happened, so check the state before resending.INTERNAL_ERROR: an unhandled failure inside the resolver, masked before it reaches you. Retryable.
Tokenization error codes
Code
retryable
Meaning
INVALID_INPUT
false
Required fields missing or malformed, or an invalid oclId. From getOclTermsMessage, also: no agreement at agreementKey, or a contentHash that doesn't match the stored agreement
INVALID_METADATA
false
agreementData or the metadata JSON could not be parsed
MISSING_METADATA_FIELD
false
The metadata is missing a required field
UNSUPPORTED_IMAGE_TYPE
false
The image content type isn't accepted
IMAGE_UPLOAD_FAILED
false
The referenced image wasn't found in storage. Upload it before referencing its key
INVALID_TERMS_SIGNATURE
false
The signature doesn't verify against the terms message
METADATA_UPLOAD_FAILED
true
Storing the metadata failed
TERMS_MESSAGE_FAILED
true
Building the terms message failed
SIGNOFF_FAILED
true
The backend could not produce its authorization signature
INTERNAL_ERROR
true
Unexpected failure, e.g. agreement generation or reading the stored agreement
The OCL flow documented on the Tokenization API page (generateOclMembershipAgreement, getOclTermsMessage) emits INVALID_INPUT, INVALID_METADATA and INTERNAL_ERROR. The rest come from the IP-NFT and IPT operations on the same service.
The in-band error has no requestId to quote. When reporting a tokenization failure, include the operation name, the time of the request and the error.message.
Contract reverts (AlreadyTokenized(), MustControlLab()) are a separate channel: they come from the onchain transaction, not from the API. See Tokenization API › Error Handling.
x402 Gateway errors
A call through the x402 Gateway can fail in two places: in the gateway itself, which returns a non-200 status with its own small body, or in the forwarded Labs mutation, which returns 200 with the usual in-band error. Check the HTTP status first.
Gateway responses
Every error body the gateway builds itself has the same shape: {"isSuccess": false, "message": "…"}. There's no code field. One 500 also carries an error string with internal detail; ignore it and read message.
Status
message
What happened
What to do
402
Payment required
No payment header (Payment-Signature, X-Payment or Payment) was sent. The payment requirements are in the base64-encoded payment-required header, not the body
Normal first step of the handshake. Read the challenge, sign, resend
402
Payment verification failed
A payment was sent, but the facilitator rejected it, for example because the amount, asset, network or payTo doesn't match the challenge, the authorization expired, or the payload was already used
Re-read the challenge, build and sign a fresh payment, resend. Nothing was charged and the mutation didn't run
400
Missing path parameter: mutation, Mutation '<name>' is not enabled for x402 gateway, Request body is required, Body must include GraphQL mutation in 'query'
The request is malformed or targets a mutation that isn't on the allow-list
Fix the request. Nothing was charged
400
Route configuration error: payment should be required
The gateway is misconfigured for this mutation. Not something your request caused
Report it. Nothing was charged and the mutation didn't run
400
Unable to determine payer address from verified payment payload
The payment verified, but no valid payer address could be resolved from it
Check that the signed authorization carries a valid from address (see Payer address resolution). Nothing was charged and the mutation didn't run
500
The parse or validation reason, e.g. Path mutation '<path>' does not match GraphQL field '<field>', Only GraphQL mutation operations are accepted, Mutation must contain exactly one top-level field
The body isn't valid JSON, or the query isn't exactly one mutation whose single top-level field matches the path
Fix the document. Rejected before payment is verified, so nothing was charged
500
Any other message
An unexpected gateway failure. It can happen after the mutation ran, for example while contacting the facilitator to settle
Don't resend blindly. Read back the state the mutation would have changed first (see Retrying)
Upstream 4xx/5xx
The upstream body, as-is
The Labs API rejected the forwarded request at the HTTP level
Settlement is skipped, so nothing was charged. Handle it as the upstream error
PAYMENT_REQUIRED is not a code any Molecule response carries. A missing or rejected payment is HTTP 402. If your client maps errors into a single code space, map 402 to your own PAYMENT_REQUIRED value, and keep the two 402 cases apart: if you sent no payment header, it's the normal handshake; if you sent one, the payment was rejected.
Forwarded mutation
A 200 body is the Labs API response verbatim. Read it with the Labs mutation contract: success is error == null; otherwise branch on error.code.
A 200 is settled even when the mutation failed. Settlement is triggered by the upstream 2xx, not by mutation success, so a 200 with error.code: "UNAUTHORIZED" or "VALIDATION_FAILED" has been paid for. Payment buys a short-lived service token for the payer wallet; it doesn't grant a role. Validate the target lab, your role on it (listLabMembers) and your inputs before signing.
One exception: a 200 with a RATE_LIMITED error didn't run. The gateway calls the Labs API with its own credential, which has a rate limit like any other. A denial comes back as HTTP 200 with a top-level errors[] entry whose errorType is RATE_LIMITED. The mutation didn't run, but the request was still settled, because settlement follows the HTTP status. Wait errorInfo.details.retryAfterSeconds, then resend with a freshly signed payment. Rate limits aren't enforced yet, so you won't see this today.
Settlement result
The settlement outcome comes back in the payment-response response header on 200 responses: base64-encoded JSON. It has success, plus transaction and network on success, or errorReason on failure.
If settlement fails, the gateway still returns 200 with the mutation's result. The mutation already ran, and you weren't charged for it. Don't re-sign and resend: that runs the mutation a second time (a second lab, a second file version). Treat the mutation result as final and log the errorReason.
Retrying
Labs or Tokenization error with retryable: true
Yes, with exponential backoff and jitter, and a cap on attempts
RATE_LIMITED
Yes, after details.retryAfterSeconds, then with backoff capped at budgetWindowSeconds. See Rate-limited requests
retryable: false
No. Change the request, or wait for the resource state to change
Labs UNAUTHORIZED just after an onchain role grant
Yes, with backoff for a few seconds; indexer lag, not a real denial
INTERNAL_ERROR / TOKEN_GENERATION_FAILED on generateServiceToken
No. Flagged retryable but permanent; fix expiresIn
x402 402 Payment verification failed
Yes, with a freshly signed payment. Never replay the same payload
x402 200 with a top-level RATE_LIMITED error
Yes, after retryAfterSeconds, with a freshly signed payment. The mutation didn't run, but the first payment was settled
x402 200, any other body
No automatic resend. The mutation ran and the request was settled (or failed to settle, which still means it ran)
x402 500 with an unexpected message
Only after reading back that the mutation didn't take effect
Retrying a mutation is only safe when you know it didn't take effect. On the Labs API, a retryable in-band error means the mutation failed. Through x402, a 200 or a late 500 can mean it succeeded.
Unknown codes
Codes may be added over time, and each addition is published in the API Changelog. When you get a code you don't recognise, from any of the three APIs:
Treat it as non-retryable.
Keep the raw value, and the
requestIdif there is one, for diagnostics.Surface it to a human rather than guessing at a recovery.
Reporting a problem
Labs API
requestId (in error.requestId or errorInfo.requestId)
Tokenization API
operation name, request time, error.code and error.message
x402 Gateway
the HTTP status and message; on a 200, the upstream requestId and the settlement transaction hash from payment-response
Related
Authentication — credentials, headers, and which role each operation needs
Rate & Query Limits — the budgets and query limits behind
RATE_LIMITED,COMPLEXITY_LIMIT_EXCEEDEDandQueryDepthLimitReachedAPI Types — the full
ApiErrorandEvmTokenizationErrordefinitionsAPI Changelog & Migration — new codes, and the
details.reasonvalues under eachAgent one-pager — the error contract condensed for agents
Last updated
Was this helpful?