Skip to content

Error Codes

GraphQL errors are returned in the standard errors array:

{
"data": null,
"errors": [
{
"message": "Not authenticated",
"path": ["me"],
"extensions": {
"code": "UNAUTHENTICATED"
}
}
]
}
CodeHTTP StatusDescription
UNAUTHENTICATED200Missing or invalid access token
FORBIDDEN200Insufficient permissions for this operation
NOT_FOUND200Requested resource does not exist
VALIDATION_ERROR200Input validation failed — check the message for details
IDEMPOTENCY_CONFLICT200Duplicate request with conflicting payload
KYB_REQUIRED200Operation requires completed KYB verification
RATE_LIMITED429Too many requests — back off and retry
Attachment codes such as STORAGE_QUOTA_EXCEEDED200A file upload or confirm was refused. Arrives in details.code: see the Errors table under Attachments

A new account can build everything (drafts, customers, products, settings, test-mode work) before confirming its email address, but anything that reaches someone outside the account, or creates a credential that outlives the session, waits for that confirmation. Until the merchant clicks the link in the welcome email (resend it from the dashboard banner or Settings), these are refused. The refusal applies to the owner, to staff acting for that merchant, and to API keys of that merchant, including keys created before the rule existed.

SurfaceRefused until verified
GraphQLsetTestMode(enabled: false), createApiKey, createStaff, createWebhookEndpoint, updateWebhookEndpoint, subscribeToPlan, changePlanAtRenewal, reactivatePlan, setPaymentsAddon(enabled: true), createPlatformCardCheckout, addOrUpdateWallet, sendInvoice, sendInvoiceReminder, batchSendInvoices, sendEstimate, sendPaymentLink, sendRefundReceipt, resendKYBLinkToEmail, startStripeConnect
RESTPOST /api/v1/invoices/{id}/send, POST /api/v1/invoices/{id}/remind, POST /api/v1/estimates/{id}/send, POST /api/v1/refunds/{reference}/send-receipt, POST and PATCH /api/v1/webhook-endpoints, POST /api/v1/api-keys, POST /api/v1/staff

Turning things off is never gated: returning to test mode, turning the payments add-on off, deleting webhook endpoints, revoking API keys and cancelling a plan all work unverified. Automatic reminders are not sent for an unverified account.

Over GraphQL the code arrives in details.code:

{
"errors": [
{
"message": "Verify your email address to do this. We sent a link to you@business.com; you can resend it from Settings.",
"extensions": {
"code": "PLIRIN_ERROR",
"details": { "code": "EMAIL_NOT_VERIFIED", "action_required": "verify_email" }
}
}
]
}

Over REST it is a 403 with error.code email_not_verified (the SDKs raise it as a PermissionError with that .code).

CodeMeaning
TURNSTILE_REQUIREDA signup, sign-in, password reset or verification resend arrived without a Cloudflare Turnstile token. Complete the check on the form and send again.
TURNSTILE_FAILEDThe token did not pass (expired, already used, or from another form). Refresh the check and send again.
TURNSTILE_UNAVAILABLEThe check is not configured on the server, so these operations are paused. Nothing to fix on your side.

API keys never meet these codes.

CodeWhereMeaning
EMAIL_NOT_ALLOWEDcreateMerchant, createStaff, REST staff create and email changeThe address is at a reserved or disposable email domain, or (for staff) already belongs to a merchant or a Plirin admin. One message for every case: “Please use a work or personal email address you can receive mail at.”
UNKNOWN_ROLEcreateStaff, updateStaff, assignRoleToStaff, REST staff routesA role other than Manager or Cashier (matched ignoring case). The message names the valid roles.

A staff member may grant only roles whose permissions they hold themselves, and may not change their own roles; either is refused as a permission error.

Drafting an invoice from a document is a dashboard feature: its GraphQL operations are not available to API keys. Its codes arrive in details.code on the operation’s error, or in a draft’s failureCode.

CodeWhereMeaning
DRAFTS_DISABLEDstart, workerDocument drafts are off: the platform switch, the configuration, or the merchant’s own AI setting. A queued draft that hits this is failed and refunded without being read.
DRAFT_PLAN_REQUIREDstartThe plan includes no drafts (Starter and free).
DRAFT_LIMIT_REACHEDstart, retryThis month’s drafts are used up.
DRAFT_TEMPORARILY_UNAVAILABLEstart, retryThe platform’s daily limit is reached, or it cannot be checked. Nothing is charged.
DRAFT_TYPE_NOT_ALLOWEDstartNot a PDF, PNG, JPEG, WebP, CSV or text file.
ATTACHMENT_TOO_LARGEstartOver 10 MB.
DRAFT_INPUT_INVALIDstart, acceptAn empty upload, or an accept form with an unknown field, a non-decimal amount, overlong text or no customer.
CURRENCY_MISSINGacceptThe form has no currency. A draft never defaults one.
DRAFT_NOT_FOUNDany by idNo such draft for this account in this mode (test or live).
DRAFT_NOT_READYaccept, dismissThe draft is not ready to accept, or already accepted, dismissed or expired.
DRAFT_BUSYdismissThe draft is being read right now. Try again shortly.
DRAFT_NOT_RETRYABLEretryOnly a failed draft whose document still exists can be retried.
DRAFT_TOO_MANY_PAGES, DRAFT_UNREADABLE, DRAFT_UPLOAD_MISSINGfailureCodeThe document has over 10 pages, cannot be read (encrypted or damaged), or never arrived. Refunded.
DRAFT_MODEL_TIMEOUT, DRAFT_MODEL_ERROR, DRAFT_INVALID_OUTPUTfailureCodeReading failed after one retry. Refunded; the draft can be retried.
DRAFT_WORKER_ABANDONED, STORAGE_UNAVAILABLEfailureCodeReading was interrupted, or storage was unavailable. Refunded.

Importing invoices is a dashboard feature: its GraphQL operations are denied to API keys. Its codes arrive in details.code. The full list, by operation, is under Invoice import.

CodeMeaning
IMPORT_DISABLEDInvoice import is not switched on for the platform
EMAIL_NOT_VERIFIEDThe account’s email address is not verified
IMPORT_CAP_EXCEEDEDThe file has more valid invoices than the plan still allows
CHASE_PLAN_REQUIREDAutomatic reminders are on paid plans
CHASE_NOT_OVERDUEThe import is not yet overdue, so it follows the normal reminder schedule and has no chase to change
IMPORTED_INVOICE_DISCOUNT_FIXEDA promotion code cannot be applied to, previewed on or cleared from an imported invoice

Statement operations are available to API keys (the read queries and sendCustomerStatement), so their codes are returned on every surface. On GraphQL the code arrives in extensions.code, repeated in extensions.details.code. On REST it is the code field of the error envelope, with the HTTP status below. See Customer statements for the feature.

CodeREST statusMeaning
FEATURE_NOT_IN_PLAN403The plan does not include customer statements (Growth and Scale do)
STATEMENTS_DISABLED403Customer statements are not switched on for the platform yet. They are being rolled out
EMAIL_NOT_VERIFIED403Sending a statement emails your customer, so it needs a verified account email, like every other send. REST returns email_not_verified. Previews and downloads still work
rate_limited429More than 60 statement sends in a minute for one account, counted across the dashboard, GraphQL and REST. Wait a minute and retry
STATEMENT_NO_ACTIVITY422Nothing to show for that customer, currency and period, so there is nothing to email
STATEMENT_TOO_LARGE422The period has more than 1,000 activity lines. Ask for a shorter period
CUSTOMER_HAS_NO_EMAIL422The customer has no email address, so a statement cannot be emailed. It can still be downloaded
STATEMENT_NOT_SENT409The PDF of a sent statement was requested for one that is still sending or failed, so no copy exists
EMAIL_SEND_FAILED502The email provider failed. The statement is kept as failed with its own number, and no email was sent. GraphQL carries the record’s statement_id and number in extensions.details; REST and the SDKs do not, so find the failed record with GET /v1/customers/{id}/statements. Neither GraphQL nor REST retries it, but the SDKs retry a 5xx by default, and each attempt takes the next statement number and leaves its own failed record (see the SDKs)

A request for a customer or statement that is not yours, or is in the other environment, is a plain 404 (resource_not_found). A malformed currency, date or cc, a to before from, or a note over 500 characters is a 400 (invalid_request).

{"errors": [{"message": "Not authenticated"}]}

Fix: Obtain a new access token via the login mutation.

{"errors": [{"message": "KYB verification required for mainnet operations"}]}

Fix: Complete KYB verification before attempting mainnet payments.

{"errors": [{"message": "Customer email is required"}]}

Fix: Check required fields in the mutation input and provide all required values.

HTTP 429 with Retry-After header. Fix: Wait the specified number of seconds before retrying.

More than one sign-in operation in a request

Section titled “More than one sign-in operation in a request”

HTTP 400. A request may name at most one sign-in or account operation (login, staffLogin, merchantLogin, adminLogin, createMerchant, requestPasswordReset, resetPassword, verifyEmail, resendVerificationEmail, refreshToken, changePassword), aliases included, and a JSON array of operations (query batching) is not accepted. Fix: Send each operation in its own request.