Error Handling for the eInvoice API

The error response shape, the HTTP status and common error code tables, the automatic retry schedule, how to diagnose an invoice that is not issued and how to check before resending.

||

This page collects everything about eInvoice API errors in one place: the error response shape, the meaning of each error code, the automatic retry mechanism, and how to proceed when an invoice is not issued.

Two kinds of error

Synchronous errorAsynchronous error
Where you see itIn the response of the API callIn the status API response, when status is Failed
Was the invoice createdNot createdUndetermined, check before resending
Retried automaticallyNo, the request has already endedYes, if the cause is transient

Error response shape

Every endpoint returns errors in the same shape:

{
  "success": false,
  "error": {
    "code": "EINVOICE_DOCUMENT_EXISTED",
    "message": "Hệ thống đã ghi nhận hoá đơn cho mã tham chiếu này."
  }
}

Handle errors based on error.code, not by matching the error.message string. message is Vietnamese text intended for display or logging, and its wording may change.

Error classification

HTTP errors

StatusMeaningHow to handle
400Wrong parameter or missing required fieldRead error.code to identify the field, correct it and call again
401Token missing, invalid, or expiredGet a new one with POST /v1/token, a token is valid for 24 hours
404Object not foundCheck the reference_code, tracking_code or the ID in the URL
405Wrong HTTP methodCreate and issue endpoints use POST, lookup endpoints use GET
409Conflict: the object already exists or is in useCheck first, see Before you resend
422Syntactically valid but invalid on business groundsUsually an invalid original invoice, an unapproved tax registration, or a wrong invoice date
500System error, or an account state errorHandle it by error.code, or contact SePay if there is no recognisable code
A 500 can be an account state error

This API also returns 500 for account state errors such as QUOTA_HAS_BEEN_USERD_UP, REGISTRATION_NOT_COMPLETE and BILLING_IS_UNPAID. Those must be resolved on my.sepay.vn, calling again does not change the result. Only a 500 with no recognisable code is a genuine system error, and in that case contact SePay with the tracking_code or reference_code.

Common error codes

error.codeHow to handle
VALIDATION_ERRORMissing required field or wrong format. message names the rejected field
TOKEN_EXPIREDThe token is older than 24 hours. Get a new one, see Authentication
EINVOICE_DOCUMENT_EXISTEDThe reference_code already has an invoice. See Before you resend
QUOTA_HAS_BEEN_USERD_UPQuota used up. Top up and call again, see Usage Quota
REGISTRATION_NOT_TAX_APPROVEDThe tax registration is not approved. Cannot be resolved through the API
INVOICE_DATE_BEFORE_TAX_APPROVALInvoice date precedes the approval date. See tax_authority_approved_date
REGISTRATION_NOT_COMPLETEPlan registration is incomplete. Resolve it on my.sepay.vn
BILLING_IS_UNPAIDThe plan is unpaid. Resolve it on my.sepay.vn
INVALID_ORIGIN_INVOICEThe original invoice must be issued, not replaced or cancelled
ISSUED_DATE_INVALIDissued_date must be the current date
INVALID_ISSUED_DATEissued_date must use the Y-m-d H:i:s format
SELLER_STORE_NOT_FOUNDThe seller_store_xid does not exist
SELLER_STORE_INACTIVEThe location is deactivated and cannot be used for new invoices
EINVOICE_ACCOUNT_NOT_FOUNDWrong provider_account_id
EINVOICE_DRAFT_NOT_FOUNDNo draft invoice found for the reference_code

QUOTA_HAS_BEEN_USERD_UP is spelled exactly as the API returns it, typo included.

Each endpoint also has its own Error Handling table listing every code it can return, for example Issue Invoice and Issue from Draft.

Retry schedule

The create and issue APIs are asynchronous: the response returns a tracking_code and the remaining processing runs in the background. When that background work hits a transient error, SePay retries automatically, with the gap between attempts growing on a Fibonacci curve:

AttemptWaitElapsed
1 (initial)immediate0 min
21 min1 min
31 min2 min
42 min4 min
53 min7 min
65 min12 min
78 min20 min
8 (last)13 min33 min

That is 8 attempts in total (the first one plus 7 retries), spanning roughly 33 minutes if every attempt fails.

Besides the attempt count there is a second limit: a request created more than 2 hours ago is no longer retried, even if attempts remain. It is a safety net for an attempt that hangs. This limit is normally never reached, because the 33-minute schedule finishes first.

Only infrastructure errors are retried: the invoice provider returns 5xx, a timeout, a connection loss, or an attempt interrupted midway. An error caused by the data or the account state returns the same result on every attempt, so the system returns Failed immediately.

The time is the earliest, not the exact moment

The cron scan runs on a 30-second cycle, so a retry can land a few dozen seconds after next_retry_at. Do not use next_retry_at as a hard timeout.

The retry block in the status response

When status is Pending and the request has hit a transient error at least once, both status APIs return an extra retry block:

{
  "status": "Pending",
  "retry": {
    "retries_count": 2,
    "max_retries": 7,
    "next_retry_at": "2026-09-04 15:12:30"
  }
}
FieldMeaning
retries_countNumber of retries already run
max_retriesMaximum number of retries, currently 7
next_retry_atExpected time of the next retry, Vietnam time

Two points are easy to misread:

  • No retry block: the request has not failed yet and is waiting for its first attempt, not that retries are disabled.
  • next_retry_at set to null: no further retry is scheduled, because all 7 retries are spent or the 2-hour window has passed, and the final result is being settled. It does not mean a retry is imminent.

Diagnosing an invoice that is not issued

Check the following in order. The cause is usually in the first few steps.

1. Check the response

success: false is a synchronous error, read error.code against the tables above. success: true with a data.tracking_code means the request was accepted, continue to step 2.

2. Check the environment

Production is https://einvoice-api.sepay.vn, Sandbox is https://einvoice-api-sandbox.sepay.vn. A token only works on the environment that issued it, and a Sandbox token sent to Production returns 401. See Sandbox.

3. Check the token validity

A token is valid for 24 hours. A long-running process reusing a token cached from the previous day receives 401 TOKEN_EXPIRED partway through. Fetch a new token on 401, rather than on every request.

4. Check status

Call Creation Status or Issuance Status with the tracking_code.

statusMeaningHow to handle
PendingBeing processed, or waiting for a retryKeep polling, read the retry block for the attempts left
SuccessCompletedFetch the invoice through Invoice Details
FailedStoppedRead message for the reason, see Before you resend

5. The invoice is still a draft

Sending is_draft: true on create leaves the invoice as a draft: it is not sent to the tax authority and does not count against the quota. Issue from Draft must be called to issue it.

6. Check the quota

Once the quota is exhausted, every issuance request returns Failed for the same reason. Call the Usage Quota API to see the remaining balance.

7. Check the tax authority approval

While the registration is unapproved, no invoice can be issued. Check tax_authority_approved_date in List Providers. The invoice date must also be on or after that date.

Before you resend

Failed does not mean that no invoice was created. Check before resending:

  1. Call Invoice Details with your reference_code.
  2. A 404: no invoice was recorded, resending is safe.
  3. Invoice data: the invoice already exists, do not resend. Read its status to see whether it still needs to be issued or is already complete.
Do not resend when the result is undetermined

When the message of a Failed request is "Hệ thống chưa xác nhận được kết quả phát hành với nhà cung cấp hóa đơn", the system could not determine whether the invoice reached the tax authority, not that it did not.

Resending in this case can produce a duplicate invoice that is already signed and filed with the tax authority. Cancelling a filed invoice is far more costly than waiting for SePay to check. Keep the tracking_code and contact SePay.

The Issue Invoice API guards against this case: resending the same reference_code after an invoice has been recorded for it returns 409 EINVOICE_DOCUMENT_EXISTED. This guard is not a substitute for the check above. Generate one unique reference_code per invoice, and reuse exactly that code when resending.

Frequently asked questions

Next