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 error | Asynchronous error | |
|---|---|---|
| Where you see it | In the response of the API call | In the status API response, when status is Failed |
| Was the invoice created | Not created | Undetermined, check before resending |
| Retried automatically | No, the request has already ended | Yes, 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
| Status | Meaning | How to handle |
|---|---|---|
| 400 | Wrong parameter or missing required field | Read error.code to identify the field, correct it and call again |
| 401 | Token missing, invalid, or expired | Get a new one with POST /v1/token, a token is valid for 24 hours |
| 404 | Object not found | Check the reference_code, tracking_code or the ID in the URL |
| 405 | Wrong HTTP method | Create and issue endpoints use POST, lookup endpoints use GET |
| 409 | Conflict: the object already exists or is in use | Check first, see Before you resend |
| 422 | Syntactically valid but invalid on business grounds | Usually an invalid original invoice, an unapproved tax registration, or a wrong invoice date |
| 500 | System error, or an account state error | Handle it by error.code, or contact SePay if there is no recognisable code |
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.code | How to handle |
|---|---|
VALIDATION_ERROR | Missing required field or wrong format. message names the rejected field |
TOKEN_EXPIRED | The token is older than 24 hours. Get a new one, see Authentication |
EINVOICE_DOCUMENT_EXISTED | The reference_code already has an invoice. See Before you resend |
QUOTA_HAS_BEEN_USERD_UP | Quota used up. Top up and call again, see Usage Quota |
REGISTRATION_NOT_TAX_APPROVED | The tax registration is not approved. Cannot be resolved through the API |
INVOICE_DATE_BEFORE_TAX_APPROVAL | Invoice date precedes the approval date. See tax_authority_approved_date |
REGISTRATION_NOT_COMPLETE | Plan registration is incomplete. Resolve it on my.sepay.vn |
BILLING_IS_UNPAID | The plan is unpaid. Resolve it on my.sepay.vn |
INVALID_ORIGIN_INVOICE | The original invoice must be issued, not replaced or cancelled |
ISSUED_DATE_INVALID | issued_date must be the current date |
INVALID_ISSUED_DATE | issued_date must use the Y-m-d H:i:s format |
SELLER_STORE_NOT_FOUND | The seller_store_xid does not exist |
SELLER_STORE_INACTIVE | The location is deactivated and cannot be used for new invoices |
EINVOICE_ACCOUNT_NOT_FOUND | Wrong provider_account_id |
EINVOICE_DRAFT_NOT_FOUND | No 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:
| Attempt | Wait | Elapsed |
|---|---|---|
| 1 (initial) | immediate | 0 min |
| 2 | 1 min | 1 min |
| 3 | 1 min | 2 min |
| 4 | 2 min | 4 min |
| 5 | 3 min | 7 min |
| 6 | 5 min | 12 min |
| 7 | 8 min | 20 min |
| 8 (last) | 13 min | 33 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 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"
}
}
| Field | Meaning |
|---|---|
retries_count | Number of retries already run |
max_retries | Maximum number of retries, currently 7 |
next_retry_at | Expected time of the next retry, Vietnam time |
Two points are easy to misread:
- No
retryblock: the request has not failed yet and is waiting for its first attempt, not that retries are disabled. next_retry_atset tonull: 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.
status | Meaning | How to handle |
|---|---|---|
Pending | Being processed, or waiting for a retry | Keep polling, read the retry block for the attempts left |
Success | Completed | Fetch the invoice through Invoice Details |
Failed | Stopped | Read 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:
- Call Invoice Details with your
reference_code. - A
404: no invoice was recorded, resending is safe. - Invoice data: the invoice already exists, do not resend. Read its
statusto see whether it still needs to be issued or is already complete.
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
- Creation Status: the full status API response
- Issuance Status: the full status API response
- Invoice Details: look up an invoice by
reference_code - Usage Quota: remaining issuance balance
- Sandbox: test environment to use before going live