Skip to content
LogoLogo

Errors

The Picked API returns an error in the standard GraphQL format. This guide covers the shape of an error response, every error code, and what your code must do with each one.

Error response shape

When an operation fails, the API returns an errors array in the response body. Each error object holds these fields:

{
  "data": null,
  "errors": [
    {
      "message": "A human-readable description of the error",
      "path": ["mutationName"],
      "extensions": {
        "code": "ERROR_CODE"
      }
    }
  ]
}
FieldDescription
messageA description of the error that a person can read. Put it in your log. Do not write code against it.
pathThe path to the field in the query that made the error.
extensions.codeAn error code that a machine can read. Write your code against this field.

One response can hold more than one error. The data field is null when the whole operation failed. It holds part of the data when only some fields failed.

Authentication error codes

These errors come from a session, an account, OAuth, a magic link, a password operation, or MFA. They match the AuthErrorCode enum in the schema.

CodeDescription
INVALID_CREDENTIALSThe email address and the password do not match.
EMAIL_NOT_CONFIRMEDThe account is there, but nobody verified the email address. See Authentication.
USER_NOT_FOUNDThere is no account with that email address.
USER_ALREADY_EXISTSAn account with that email address is there already. The API returns this when you create an account.
WEAK_PASSWORDThe password is not strong enough.
INVALID_TOKENThe token is invalid or malformed. This covers a verification token, a reset token, and a magic link token.
TOKEN_EXPIREDThe token expired. Ask for a new one.
MFA_CHALLENGE_FAILEDThe MFA verification code is not correct.
MFA_FACTOR_NOT_FOUNDThat MFA factor is not there, or it belongs to another caller.
MFA_REQUIREDThe user must verify a code from an authenticator app before this operation. Complete the challenge, then use the returned session.
OAUTH_PROVIDER_ERRORThe exchange with the OAuth provider, such as Google, failed.
RATE_LIMITEDToo many requests in a short period. Wait, then try again.

Example: invalid credentials

{
  "data": {
    "sessionCreate": {
      "session": null
    }
  },
  "errors": [
    {
      "message": "Invalid credentials",
      "path": ["sessionCreate"],
      "extensions": {
        "code": "INVALID_CREDENTIALS"
      }
    }
  ]
}

Authorization error codes

An authorization error happens when the caller does not hold the scope that an operation needs.

CodeDescription
UNAUTHORIZEDThe request carries no valid authentication. It has no Authorization header and no x-api-key header, or the token is invalid.
FORBIDDENThe caller is authenticated, but it does not hold the scope that the operation needs.

Example: the scope is not enough

A caller that holds organization/read only tries to delete an organization. It receives this:

{
  "data": {
    "organizationDelete": null
  },
  "errors": [
    {
      "message": "Insufficient permissions",
      "path": ["organizationDelete"],
      "extensions": {
        "code": "FORBIDDEN"
      }
    }
  ]
}

Organization, member, and invitation error codes

These errors come from an organization, a member operation, or an invitation. They match the ErrorCode enum in the schema.

Organization errors

CodeDescription
ORGANIZATION_NOT_FOUNDThat organization is not there, or the caller cannot reach it.
ORGANIZATION_ALREADY_EXISTSAn organization with that name is there already.
SLUG_ALREADY_EXISTSAnother organization uses that slug already.
INVALID_SLUGThe slug holds an invalid character. A slug takes lowercase letters, digits, and hyphens only.

Member errors

CodeDescription
MEMBER_NOT_FOUNDThat user is not a member of the organization.
MEMBER_ALREADY_EXISTSThe user is a member of the organization already.
CANNOT_REMOVE_PRIMARY_OWNERYou cannot remove the primary owner of an organization.
CANNOT_CHANGE_OWN_ROLEA member cannot change the role of that same member. Another owner must do it.

Invitation errors

CodeDescription
INVITATION_NOT_FOUNDThat invitation is not there.
INVITATION_EXPIREDThe invitation expired. Nobody can accept it now.
INVITATION_ALREADY_ACCEPTEDSomebody accepted the invitation already.

Project errors

CodeDescription
PROJECT_NOT_FOUNDThat project is not there, or the caller cannot reach it.

General errors

CodeDescription
VALIDATION_ERRORThe input failed a validation rule. Read the message field to learn which field is invalid.
INTERNAL_ERRORThe server met an unexpected error. Try the request again. Contact support when the error continues.

What your code must do

Look for an error first

Check every response for an errors array before you read data:

const response = await fetch('/graphql', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${accessToken}`,
  },
  body: JSON.stringify({ query, variables }),
});
 
const result = await response.json();
 
if (result.errors) {
  for (const error of result.errors) {
    const code = error.extensions?.code;
 
    switch (code) {
      case 'UNAUTHORIZED':
        // Send the user to the sign-in page.
        break;
      case 'FORBIDDEN':
        // Show a permissions error.
        break;
      case 'RATE_LIMITED':
        // Wait, then try again.
        break;
      default:
        // Put the message in the log, or show it.
        console.error(error.message);
    }
  }
}

Refresh a token that expired

A request can return UNAUTHORIZED while the caller holds a refresh token. Try a session refresh first. Do not treat it as a failure to sign in:

if (code === 'UNAUTHORIZED' && refreshToken) {
  const refreshResult = await sessionRefresh(refreshToken);
  if (refreshResult.session) {
    // Try the first request again with the new access token.
  } else {
    // Send the user to the sign-in page.
  }
}

Read data that is only part complete

GraphQL can return part of the data together with an error. Take a query that reads an organization and its members. It can succeed for the organization and fail for the members, when the caller does not hold organization_members/read. The response then holds data with the organization, and errors for the members field. Always read both.

Try again after a transient failure

INTERNAL_ERROR and RATE_LIMITED are both transient. For RATE_LIMITED, use an exponential backoff before you try again. For INTERNAL_ERROR, one more try is usually enough.