Platform Error Codes¶
Applies to: All subscriptions
Purpose¶
Interpret the coded errors Complifly itself returns — authentication, authorisation, document state — and know immediately which are transient, which are terminal, and who resolves each.
Audience¶
Integration developers, administrators, support teams.
Prerequisites¶
- The error code and the request identifier
- Error Code Reference
Reference¶
Authentication¶
| Code | HTTP | Meaning | Fixed by | Retry? |
|---|---|---|---|---|
TOKEN_MISSING |
401 | No credential presented | you | No — send the header |
TOKEN_INVALID |
401 | Malformed, wrong audience, wrong issuer, or a bad signature | you | No — fix the credential |
TOKEN_EXPIRED |
401 | Expired, or the clocks disagree | you | Yes, once, after refreshing |
OAUTH_REQUIRED |
403 | This client has been switched to identity-provider tokens; the static key is refused | admin | No — complete the migration |
ROLE_MISSING |
403 | The token is valid but lacks the required application role | admin | No — grant admin consent |
CLIENT_NOT_ONBOARDED |
403 | The token's directory and application together are not a connection Complifly serves, or the connection is not live | admin | No — finish onboarding |
TOKEN_EXPIRED recurring after a refresh points at clock drift, not at the credential. Token validation is time-sensitive; check time synchronisation on both hosts.
CLIENT_NOT_ONBOARDED is about the pair, not the application¶
Complifly recognises an integration as this application, in this directory. An application ID on its own identifies nobody: the same application can be installed in many directories and keeps the same ID in each.
So this code has several distinct causes that look identical from the outside, and the message deliberately does not say which — telling an unauthenticated caller which half matched would let anyone enumerate other customers' integrations.
| Cause | How to tell |
|---|---|
| Onboarding never finished — Complifly has your directory but not your application ID | Ask for the connection's stage. AWAITING_CLIENT means exactly this |
| The ERP moved to a different app registration | Compare the appid in your token against what you sent Complifly |
| The token came from a different directory than the one recorded | Compare the tid in your token against your Directory (tenant) ID |
| The connection was suspended | Ask your Complifly contact |
Decode the token and check tid and appid before reporting a fault. Between them they identify the connection, and the answer is almost always visible there.
Authorisation¶
| Code | HTTP | Meaning | Fixed by | Retry? |
|---|---|---|---|---|
GSTIN_NOT_PERMITTED |
403 | The document's supplier registration is outside the credential's scope | admin | No |
FORBIDDEN |
403 | The document does not belong to this credential's company | admin | No |
| Missing registration header | 400 | A user-token call omitted the registration header | you | No |
GSTIN_NOT_PERMITTED is a credential scope problem, not a user permission problem. It is the single most misdiagnosed platform error: teams check the user's role, find it correct, and conclude the system is broken.
Where that scope comes from depends on the credential, which is worth knowing before you go looking for it:
| Credential | Scope |
|---|---|
| OAuth connection | The GSTIN list attached to that connection, set at onboarding |
| Static key with a recorded GSTIN | That one registration |
| Static key without one | Every GSTIN registered under the key's company |
A registration added to the company after an OAuth connection was onboarded is not automatically in that connection's list. This is deliberate — an ERP's authority should not widen because someone registered a new GSTIN elsewhere — but it does mean a new registration needs the connection updated.
Document state¶
| Code | HTTP | Meaning | Fixed by | Retry? |
|---|---|---|---|---|
DUPLICATE |
409 | The document number already exists | you | Never — permanent |
NOT_FOUND |
404 | No such document | you | No |
IRN_ALREADY_CANCELLED |
— | The reference number is already cancelled | you | No |
STATUS_LOCK |
— | The document is in a state that does not allow this action | you | No |
INDETERMINATE_PAYLOAD_CHANGED |
— | A previous attempt's outcome is unknown, and the document has since changed | support | No |
LOCAL_VALIDATION |
— | Validation failed before submission | you | No |
NIC_PRINT_WINDOW_CLOSED |
422 | The official NIC print copy is no longer obtainable | statute | Never |
NIC_PRINT_WINDOW_CLOSED deserves its own explanation, because the underlying provider message reads like a fault to fix and is not. The government serves the official e-invoice copy for two days after generation. Complifly fetches and caches it at generation time so it survives that window — but where no copy was cached, it becomes permanently unobtainable.
Retrying cannot help, and neither can support. Use Branded Print, which renders from the signed invoice and signed QR captured at generation; that stored evidence is what makes an e-invoice print valid in the first place.
INDETERMINATE_PAYLOAD_CHANGED deserves explanation. It arises when a submission's outcome could not be determined — a timeout, typically — and the document was then edited. The platform refuses to resubmit, because it cannot tell whether the original was registered, and resubmitting a changed payload under the same number risks either a duplicate or a document that does not match what was registered. Do not force it. Establish the true state first.
Approval workflow¶
Returned by the Invoice Generation approval endpoints when the multi-level Approval Engine (IGM_APPROVAL_ENGINE) is on.
| Code | HTTP | Meaning | Fixed by | Retry? |
|---|---|---|---|---|
ROLE_MISMATCH |
403 | The caller's approval role is not the one this level requires | admin | No — the right approver must act, or an administrator must override |
SELF_APPROVAL |
403 | The caller submitted this document and cannot also decide it | you | Never — not waivable by anyone |
NOT_PENDING |
409 | There is no open approval on this document, or no level left to act on | you | No — re-read the workflow state |
APPROVAL_PENDING |
409 | Posting was attempted while an approval is still open | you | Yes, once the approval clears |
APPROVAL_REJECTED |
409 | The document was rejected in approval and cannot post | you | Never for this document — raise a corrected one |
REASON_REQUIRED |
400 | A force-approve, force-reject, or forced engine disable was sent without the mandatory reason | you | Yes, with a reason |
APPROVALS_IN_FLIGHT |
409 | IGM_APPROVAL_ENGINE cannot be switched off while documents still await approval |
admin | Yes, once the queue is clear — or with force + reason |
ROLE_MISMATCH is usually a provisioning gap, not a bug¶
The message names the role the level requires and the role the caller actually holds. Read both halves — they distinguish the three causes:
| Message ends with | Cause | Fix |
|---|---|---|
…you have no approval role assigned |
The user has no approval_role_key |
Assign one on the user record |
…your assigned approval role is X |
The user is a real approver, but at a different level | The named level must act first. Approvals are sequential |
The call never reached this check (403 from the permission guard instead) |
The user's system role does not hold invoice.approve |
Assign the approver or tax_controller system role |
There is deliberately no administrative exemption. An org admin or Platform Super Admin whose approval role does not match is refused exactly like anyone else. The supported way out is POST /invoices/{docId}/approval/override, which requires admin.api, demands a written reason, and records the remaining levels as OVERRIDDEN rather than APPROVED — so a bypass is never indistinguishable from a real approval in the audit trail.
APPROVALS_IN_FLIGHT protects approvals already in progress¶
Switching the multi-level engine off does not merely stop future routing. Without this refusal, a document routed to three approvers and signed by none would stop being pending and post unapproved: the legacy flat gate takes over, its thresholds default to 0 (meaning off), and it rewrites the document from PENDING to NOT_REQUIRED. Nothing is rejected and nothing is logged as a bypass — the only trace is an approval_request still reading PENDING for a document already in the ledger.
The response body lists every blocking document with the role it is waiting on:
{
"code": "APPROVALS_IN_FLIGHT",
"message": "22 document(s) on 29AAACW3775F000 are still awaiting approval (waiting on finance_controller, finance_manager). …",
"open": [
{ "docNo": "INV/00042", "docId": "…", "waitingOn": "finance_controller",
"level": "1 of 3", "amount": 1180000, "submittedBy": "U0000013" }
]
}
Clear them — approve, reject, or per-document override — then retry. If a level names an approval role nobody holds, those documents can never clear and would block the setting for ever; for that case only, repeat the call with force: true and a written reason. Forcing cancels the approvals rather than approving them: every level is stamped OVERRIDDEN with the reason, the request becomes CANCELLED, and the documents return to having no approval decision at all.
SELF_APPROVAL cannot be resolved by changing a role¶
It is checked on identity, not permission: whoever submitted the document is refused, whatever role they hold, including Platform Super Admin. Granting more permissions will not change the outcome. Either another holder of the level decides it, or an administrator overrides with a reason.
Validation and ingest¶
| Code | HTTP | Meaning | Fixed by |
|---|---|---|---|
REJECTED with errors |
422 | Validation failed | you |
VALIDATION |
422 | Same, recorded as a rejection reason | you |
ERROR |
500 | Server error | support |
Write-back feed¶
| Code | HTTP | Meaning | Fixed by |
|---|---|---|---|
NO_SUBSCRIPTION |
404 | No active subscription for the credential's registrations | admin |
NO_EVENT_IDS |
422 | Acknowledgement body missing identifiers | you |
TOO_MANY |
422 | Too many identifiers in one acknowledgement | you |
FEED_ERROR |
500 | Feed query failed | support |
ACK_ERROR |
500 | Acknowledgement failed | support |
NO_SUBSCRIPTION is a configuration state, not a transient one. Retrying it forever is a common integration defect; it will never succeed until someone configures a subscription.
Rate limiting¶
| Condition | HTTP | Fixed by | Retry? |
|---|---|---|---|
| Limit exceeded | 429 | you | Yes, after backing off |
The retry summary¶
| Never retry | Retry |
|---|---|
DUPLICATE |
429 |
REJECTED and VALIDATION |
5xx, bounded |
GSTIN_NOT_PERMITTED, FORBIDDEN |
TOKEN_EXPIRED, once, after refresh |
OAUTH_REQUIRED |
Timeouts, after reconciling |
NO_SUBSCRIPTION |
|
STATUS_LOCK, IRN_ALREADY_CANCELLED |
Validation¶
| Check | Pass condition |
|---|---|
| The code is captured | Along with the request identifier |
| Authentication and authorisation are distinguished | 401 and 403 take different paths |
| Nothing terminal is retried | Verified by forcing each response |
| Scope errors reach an administrator | Not treated as a data fault |
| Support-class errors carry evidence | Request identifier and document number included |
Troubleshooting¶
| Symptom | Cause | Action |
|---|---|---|
TOKEN_EXPIRED immediately after refresh |
Clock drift | Fix time synchronisation on both hosts |
TOKEN_INVALID with a token that decodes correctly |
Wrong audience or issuer | Compare the claims against the configuration |
Valid token, 403 |
Application role not granted | Grant admin consent |
GSTIN_NOT_PERMITTED while the user's role is correct |
It is a credential scope problem | Check the credential's registration scope |
OAUTH_REQUIRED on a working key |
The client was switched to token-only | Complete the migration |
DUPLICATE on a document never sent |
Numbering restarted, or two systems share a series | Fix the series. Numbers are consumed permanently |
STATUS_LOCK |
The document's state does not allow the action | Check its status; the action may already have occurred |
INDETERMINATE_PAYLOAD_CHANGED |
Unknown outcome plus a subsequent edit | Do not force. Establish the true state, with support if necessary |
NO_SUBSCRIPTION retried indefinitely |
Treated as transient | It is configuration. Configure a subscription |
Persistent 429 at modest volume |
Retrying terminal failures, or authenticating per call | Fix the retry policy; cache tokens |
ERROR repeatedly |
A genuine server fault | Escalate with the request identifier |
Related Articles¶
- API Authentication — the authentication codes in context
- Rate Limits and Errors — the retry policy
- Machine-to-Machine Authentication — credential scope
- Login and Access Failures