Error Codes
All errors return the envelope { ok: false, error: { code, message, field?, meta?, request_id } } with semantic HTTP statuses, never 200 for failures. The table below lists all codes you may encounter across /v1 and embed channels, synchronized with backend registries.
Error Envelope Structure
HTTP/1.1 402 Payment Required
Content-Type: application/json
X-Request-Id: 018f2b00-0000-7000-8000-0000000000ff
{
"ok": false,
"error": {
"code": "wallet.insufficient_balance",
"message": "Số dư ví không đủ để thực hiện giao dịch.",
"field": null,
"meta": null,
"request_id": "018f2b00-0000-7000-8000-0000000000ff"
}
}HTTP-native: reverse proxies, monitoring, and REST clients already branch on HTTP statuses: using standard statuses leverages existing infrastructure.
Self-describing: reading wallet.insufficient_balance is instantly understandable without looking up tables; adding new codes does not break existing logic.
Legacy system expects numbers? The code string is immutable and acts as a stable translation key for your internal tables.
4 Error Handling Best Practices
Code ↔ HTTP Mapping Table
Grouped by domain. The API reference page also specifies which endpoints return which error branches; consult alongside to identify applicable codes.
Showing 91 / 91 codes
Authentication, keys and limits partner.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
partner.invalid_key | 401 | Invalid API key. | No: verbatim retry will fail again |
partner.invalid_signature | 401 | Invalid request signature. | No: verbatim retry will fail again |
partner.timestamp_expired | 401 | The request timestamp is too far off from the server's clock. | No: verbatim retry will fail again |
partner.nonce_reused | 401 | This nonce has already been used. | No: verbatim retry will fail again |
partner.ip_not_allowed | 403 | This IP address is not on the allow list. | No: verbatim retry will fail again |
partner.scope_denied | 403 | This API key does not have permission to call this endpoint. | No: verbatim retry will fail again |
partner.key_suspended | 403 | This API key is currently suspended. | No: verbatim retry will fail again |
partner.key_revoked | 403 | This API key has been revoked. | No: verbatim retry will fail again |
partner.endpoint_disabled | 503 | This endpoint group is temporarily out of service. | Yes: retry verbatim |
partner.rate_limited | 429 | Rate limit exceeded. Please try again later. | Yes: retry verbatim |
partner.package_not_visible | 404 | This package code is not purchasable with your key. | No: verbatim retry will fail again |
partner.daily_quota_exceeded | 429 | Daily request quota exceeded. Please try again tomorrow. | Yes: retry verbatim |
partner.wrong_host | 403 | This API key cannot be used on the requested host. Test keys (`tke_test_`) must call `https://api-testing.tongkhoesim.com`, live keys (`tke_live_`) must call `https://api.tongkhoesim.com`. | No: verbatim retry will fail again |
General API errors internal.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
internal.validation_failed | 400 | The submitted data is invalid. | No: verbatim retry will fail again |
internal.invalid_filter | 400 | Invalid filter condition. | No: verbatim retry will fail again |
internal.invalid_sort_field | 400 | Invalid sort field. | No: verbatim retry will fail again |
internal.unauthenticated | 401 | You are not signed in or your session has expired. | No: verbatim retry will fail again |
internal.forbidden | 403 | You do not have permission to perform this action. | No: verbatim retry will fail again |
internal.not_found | 404 | The requested resource was not found. | No: verbatim retry will fail again |
internal.route_not_found | 404 | This path does not exist. Check the method and path against the documentation. | No: verbatim retry will fail again |
internal.method_not_allowed | 405 | This HTTP method is not supported. | No: verbatim retry will fail again |
internal.conflict | 409 | State conflict — the action could not be completed. | No: verbatim retry will fail again |
internal.unsupported_media_type | 415 | This content type is not supported. | No: verbatim retry will fail again |
internal.rate_limited | 429 | You've sent too many requests. Please try again later. | Yes: retry verbatim |
internal.server_error | 500 | A system error occurred. Please try again later. | Yes: retry verbatim |
internal.service_unavailable | 503 | The service is temporarily unavailable. Please try again later. | Yes: retry verbatim |
internal.limit_exceeded | 422 | You've reached the allowed limit. Please remove some items and try again. | No: verbatim retry will fail again |
internal.money_frozen | 503 | The system has temporarily paused financial transactions for a ledger check. Please try again later. | Yes: retry verbatim |
Plan catalog catalog.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
catalog.cursor_mismatch | 400 | This page cursor does not match the current filters. Start again from the first page (drop the cursor parameter). | No: verbatim retry will fail again |
Quotes and orders order.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
order.quote_not_found | 404 | Quote not found. | No: verbatim retry will fail again |
order.quote_expired | 409 | This quote has expired. Please create a new one. | No: verbatim retry will fail again |
order.quote_already_committed | 409 | This quote has already been committed into an order. | No: verbatim retry will fail again |
order.package_not_allowed | 400 | This package does not exist or is not available for purchase. | No: verbatim retry will fail again |
order.not_deliverable | 409 | This order isn't complete yet, so this action isn't available. | No: verbatim retry will fail again |
order.quantity_limit_exceeded | 400 | The number of eSIMs exceeds the limit allowed per order. | No: verbatim retry will fail again |
order.fulfillment_config_changed | 409 | This package's fulfillment configuration has changed. Please create a new quote. | No: verbatim retry will fail again |
order.refund_suggestion_pending | 409 | This order has a refund suggestion pending approval. Please resolve it first. | No: verbatim retry will fail again |
eSIM esim.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
esim.out_of_stock | 409 | Not enough stock in inventory for this package. | No: verbatim retry will fail again |
esim.not_cancellable | 409 | This eSIM is not in a cancellable state. | No: verbatim retry will fail again |
esim.lpa_not_available | 409 | This eSIM doesn't have an LPA code yet (not activated/registered). | No: verbatim retry will fail again |
esim.not_eligible | 409 | This eSIM is not eligible for a top-up. | No: verbatim retry will fail again |
esim.not_activated | 409 | This eSIM has not been installed yet — install and enable it before topping up. | No: verbatim retry will fail again |
esim.topup_not_supported | 409 | This package does not support top-ups. | No: verbatim retry will fail again |
esim.topup_provider_mismatch | 409 | This top-up package belongs to a different provider than the one serving this eSIM. Choose a top-up package listed for that eSIM. | No: verbatim retry will fail again |
esim.refund_not_eligible | 409 | This eSIM is not eligible for a refund request. | No: verbatim retry will fail again |
esim.refund_request_pending | 409 | This eSIM already has a refund request pending. | No: verbatim retry will fail again |
esim.suspended | 409 | This eSIM is currently suspended — please resume it before performing this action. | No: verbatim retry will fail again |
Wallet wallet.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
wallet.frozen | 409 | This wallet is frozen and cannot be spent from. | No: verbatim retry will fail again |
wallet.velocity_hold | 409 | This wallet is on hold for review. Please contact support. | No: verbatim retry will fail again |
wallet.insufficient_balance | 402 | Insufficient wallet balance to complete this transaction. | No: verbatim retry will fail again |
wallet.deleted | 409 | The destination wallet is invalid (deleted). | No: verbatim retry will fail again |
wallet.velocity_exceeded | 429 | Transactions exceeded the allowed threshold in a short time. The wallet has been put on hold for review. | No: verbatim retry will fail again |
Refund requests refund.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
refund.note_required | 400 | Please enter a reason for rejecting this refund. | No: verbatim retry will fail again |
Selling prices pricing.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
pricing.no_price_for_buyer | 409 | This package doesn't have a selling price configured for you yet. | No: verbatim retry will fail again |
eSIM provider provider.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
provider.disabled | 409 | This package's provider is disabled and cannot be sold. | No: verbatim retry will fail again |
provider.api_group_disabled | 409 | This provider's service group is currently disabled. | No: verbatim retry will fail again |
provider.not_registered | 500 | No connector is configured for this provider. | Yes: retry verbatim |
provider.unsupported | 400 | This provider does not support this action. | No: verbatim retry will fail again |
provider.timeout | 504 | The provider took too long to respond. Please try again later. | Yes: retry verbatim |
provider.auth_failed | 502 | Authentication with the provider failed. | Yes: retry verbatim |
provider.rate_limited | 429 | The provider is rate-limiting requests. Please try again later. | Yes: retry verbatim |
provider.malformed_response | 502 | The provider returned invalid data. | Yes: retry verbatim |
provider.call_failed | 502 | The call to the provider failed. | Yes: retry verbatim |
provider.out_of_stock | 409 | This package is out of stock at the provider. | No: verbatim retry will fail again |
provider.msisdn_taken | 409 | The MSISDN being provisioned already belongs to another subscriber at the provider. | No: verbatim retry will fail again |
provider.msisdn_exhausted | 409 | No usable MSISDN left at the provider. | No: verbatim retry will fail again |
Idempotency idempotency.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
idempotency.conflict | 409 | This Idempotency-Key was already used with different content. | No: verbatim retry will fail again |
Provider capabilities capability.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
capability.not_supported | 422 | This package does not support the requested feature. | No: verbatim retry will fail again |
Test mode / simulation testmode.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
testmode.live_key_forbidden | 403 | This endpoint is available in testmode only with test API keys (prefix `tke_test_`) on host api-testing. The key you used is a live key, so simulation is not possible — switch to a test key of the same organization. | No: verbatim retry will fail again |
testmode.not_test_item | 422 | This eSIM is a LIVE record — testmode only operates on test eSIMs. To cancel a real order, submit a refund request for review. | No: verbatim retry will fail again |
testmode.invalid_scenario | 422 | Invalid simulation scenario. Send at least one of `data_used_mb`, `status`, `expire_now`, `throttled`; `status` accepts only released|installed|enabled|disabled. | No: verbatim retry will fail again |
testmode.no_sellable_package | 422 | The organization does not have pricing for any package to create sample data — at least one sellable package with pricing is required. | No: verbatim retry will fail again |
Webhook webhook.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
webhook.health_check_failed | 422 | The webhook URL did not respond validly during the health check. | No: verbatim retry will fail again |
webhook.url_not_allowed | 422 | This webhook URL is not allowed (it must be a public https URL). | No: verbatim retry will fail again |
webhook.not_configured | 404 | No webhook is configured for this API key. | No: verbatim retry will fail again |
webhook.delivery_not_found | 404 | Webhook delivery not found. | No: verbatim retry will fail again |
webhook.already_configured | 409 | This API key already has a webhook endpoint. One endpoint per key — update it with `PUT /v1/webhooks/{id}`, or remove it with `DELETE /v1/webhooks/{id}` and create a new one. | No: verbatim retry will fail again |
webhook.not_found | 404 | No such webhook endpoint on the calling API key. | No: verbatim retry will fail again |
Organization status org.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
org.invoice_profile_incomplete | 422 | Organization has incomplete invoice profile (legal name, tax code, address). | No: verbatim retry will fail again |
org.suspended | 403 | This organization is currently suspended. | No: verbatim retry will fail again |
org.closed | 403 | This organization is closed. | No: verbatim retry will fail again |
Public embed channel embed.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
embed.invalid_token | 401 | Invalid embed token. | No: verbatim retry will fail again |
embed.token_revoked | 410 | This embed channel is no longer active. | No: verbatim retry will fail again |
embed.rate_limited | 429 | This embed channel is calling too fast. Please try again later. | Yes: retry verbatim |
embed.package_not_visible | 404 | Package not found. | No: verbatim retry will fail again |
VAT invoices invoice.*
| code | HTTP | Meaning | Retryable? |
|---|---|---|---|
invoice.order_not_settled | 409 | This order hasn't settled yet (a refund is still processing), so an invoice can't be issued. Please try again in a few minutes. | Yes: retry verbatim |
invoice.profile_incomplete | 422 | The organization's invoicing profile is incomplete. Please fill it in fully before issuing an invoice. | No: verbatim retry will fail again |
invoice.already_requested | 409 | This order already has an invoice. | No: verbatim retry will fail again |
invoice.nothing_to_invoice | 409 | After deducting the refunded amount, there is nothing left on this order to invoice. | No: verbatim retry will fail again |
invoice.amount_changed | 409 | This order's amount just changed due to a new refund. Please review the amount and issue the invoice again. | Yes: retry verbatim |
invoice.buyer_invalid | 422 | The receiving company's details are incomplete, or the tax code format is invalid. | No: verbatim retry will fail again |