Appearance
LHDN Submission
Submit validated documents to LHDN's MyInvois system.
Setup
Before using the examples below, set up your HTTP client:
javascript
const API_URL = 'https://invoisx.com/api/v1';
const API_TOKEN = 'your-api-token';
async function api(method, endpoint, data = null) {
const response = await fetch(`${API_URL}${endpoint}`, {
method,
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: data ? JSON.stringify(data) : null,
});
return response.json();
}python
import requests
API_URL = 'https://invoisx.com/api/v1'
API_TOKEN = 'your-api-token'
def api(method, endpoint, data=None):
response = requests.request(
method,
f'{API_URL}{endpoint}',
headers={
'Authorization': f'Bearer {API_TOKEN}',
'Accept': 'application/json',
},
json=data
)
return response.json()php
use Illuminate\Support\Facades\Http;
$api = Http::withToken('your-api-token')
->accept('application/json')
->baseUrl('https://invoisx.com/api/v1');php
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://invoisx.com/api/v1/',
'headers' => [
'Authorization' => 'Bearer your-api-token',
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
]);java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
String API_URL = "https://invoisx.com/api/v1";
String API_TOKEN = "your-api-token";csharp
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
var client = new HttpClient {
BaseAddress = new Uri("https://invoisx.com/api/v1")
};
client.DefaultRequestHeaders.Add("Authorization", "Bearer your-api-token");
client.DefaultRequestHeaders.Add("Accept", "application/json");Overview
Submission is a two-step process:
- Submit the document to LHDN
- Check status to confirm acceptance
Prerequisites
Before submitting:
- Document must be in
readystatus (call validate first) - Company must have LHDN credentials configured
- Active subscription with remaining quota
Submitting a Document
javascript
const submission = await api('POST', `/documents/${documentId}/submit`);
console.log(`Status: ${submission.data.status}`);
console.log(`LHDN UUID: ${submission.data.document_uuid}`);
console.log(`Invoice Number: ${submission.data.invoice_code_number}`);python
submission = api('POST', f'/documents/{document_id}/submit')
print(f"Status: {submission['data']['status']}")
print(f"LHDN UUID: {submission['data']['document_uuid']}")
print(f"Invoice Number: {submission['data']['invoice_code_number']}")php
$response = $api->post("/documents/$documentId/submit");
$result = $response->json('data');
echo "Status: {$result['status']}\n";
echo "LHDN UUID: {$result['document_uuid']}\n";
echo "Invoice Number: {$result['invoice_code_number']}\n";php
$response = $client->post("documents/$documentId/submit");
$result = json_decode($response->getBody(), true)['data'];
echo "Status: {$result['status']}\n";
echo "LHDN UUID: {$result['document_uuid']}\n";
echo "Invoice Number: {$result['invoice_code_number']}\n";java
var request = HttpRequest.newBuilder()
.uri(URI.create(API_URL + "/documents/" + documentId + "/submit"))
.header("Authorization", "Bearer " + API_TOKEN)
.POST(HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
// Parse JSON to get status, document_uuid, invoice_code_number
System.out.println("Response: " + response.body());csharp
var response = await client.PostAsync($"/documents/{documentId}/submit", null);
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
var data = json.GetProperty("data");
Console.WriteLine($"Status: {data.GetProperty("status").GetString()}");
Console.WriteLine($"LHDN UUID: {data.GetProperty("document_uuid").GetString()}");
Console.WriteLine($"Invoice Number: {data.GetProperty("invoice_code_number").GetString()}");Submission Response
json
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "Submitted",
"document_uuid": "QZSABHBPPZH51WM6G06KSSBK10",
"invoice_code_number": "INV-2024-001",
"message": "Document submitted successfully to LHDN"
}
}Save the document_uuid
This is LHDN's unique identifier. You'll need it for verification and as a reference for credit/debit notes.
Checking Status
After submission, LHDN validates the document. Check status to confirm:
javascript
const status = await api('GET', `/documents/${documentId}/status`);
console.log(`Status: ${status.data.status}`);
console.log(`Validated At: ${status.data.validated_at || 'Pending'}`);
if (status.data.status === 'Invalid') {
console.log('Errors:', status.data.validation_steps);
}python
status = api('GET', f'/documents/{document_id}/status')
print(f"Status: {status['data']['status']}")
print(f"Validated At: {status['data'].get('validated_at', 'Pending')}")
if status['data']['status'] == 'Invalid':
print('Errors:', status['data'].get('validation_steps'))php
$response = $api->get("/documents/$documentId/status");
$result = $response->json('data');
echo "Status: {$result['status']}\n";
echo "Validated At: " . ($result['validated_at'] ?? 'Pending') . "\n";
if ($result['status'] === 'Invalid') {
print_r($result['validation_steps']);
}php
$response = $client->get("documents/$documentId/status");
$result = json_decode($response->getBody(), true)['data'];
echo "Status: {$result['status']}\n";
echo "Validated At: " . ($result['validated_at'] ?? 'Pending') . "\n";
if ($result['status'] === 'Invalid') {
print_r($result['validation_steps']);
}java
var request = HttpRequest.newBuilder()
.uri(URI.create(API_URL + "/documents/" + documentId + "/status"))
.header("Authorization", "Bearer " + API_TOKEN)
.GET()
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
// Parse JSON to check status and validation_steps if Invalid
System.out.println("Status: " + response.body());csharp
var response = await client.GetAsync($"/documents/{documentId}/status");
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
var data = json.GetProperty("data");
var status = data.GetProperty("status").GetString();
Console.WriteLine($"Status: {status}");
if (data.TryGetProperty("validated_at", out var validatedAt)) {
Console.WriteLine($"Validated At: {validatedAt.GetString()}");
} else {
Console.WriteLine("Validated At: Pending");
}
if (status == "Invalid") {
Console.WriteLine($"Errors: {data.GetProperty("validation_steps")}");
}Status Values
| Status | Description |
|---|---|
Submitted | Sent to LHDN, awaiting validation |
Valid | Accepted by LHDN |
Invalid | Rejected by LHDN (see validation_steps for details) |
Polling for Status
LHDN validation typically takes a few seconds but can be longer during peak times:
javascript
async function waitForValidation(documentId, maxAttempts = 10) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const status = await api('GET', `/documents/${documentId}/status`);
if (status.data.status !== 'Submitted') {
return status.data;
}
console.log(`Attempt ${attempt}: Still processing...`);
await new Promise(resolve => setTimeout(resolve, 2000));
}
throw new Error('Validation timeout');
}
const result = await waitForValidation(documentId);
console.log(`Final status: ${result.status}`);python
import time
def wait_for_validation(document_id, max_attempts=10):
for attempt in range(1, max_attempts + 1):
status = api('GET', f'/documents/{document_id}/status')
if status['data']['status'] != 'Submitted':
return status['data']
print(f'Attempt {attempt}: Still processing...')
time.sleep(2)
raise Exception('Validation timeout')
result = wait_for_validation(document_id)
print(f"Final status: {result['status']}")php
function waitForValidation($api, $documentId, $maxAttempts = 10) {
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
$response = $api->get("/documents/$documentId/status");
$result = $response->json('data');
if ($result['status'] !== 'Submitted') {
return $result;
}
echo "Attempt $attempt: Still processing...\n";
sleep(2);
}
throw new Exception('Validation timeout');
}
$result = waitForValidation($api, $documentId);
echo "Final status: {$result['status']}\n";php
function waitForValidation($client, $documentId, $maxAttempts = 10) {
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
$response = $client->get("documents/$documentId/status");
$result = json_decode($response->getBody(), true)['data'];
if ($result['status'] !== 'Submitted') {
return $result;
}
echo "Attempt $attempt: Still processing...\n";
sleep(2);
}
throw new Exception('Validation timeout');
}
$result = waitForValidation($client, $documentId);
echo "Final status: {$result['status']}\n";java
public Map<String, Object> waitForValidation(String documentId, int maxAttempts) throws Exception {
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
var request = HttpRequest.newBuilder()
.uri(URI.create(API_URL + "/documents/" + documentId + "/status"))
.header("Authorization", "Bearer " + API_TOKEN)
.GET()
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
// Parse JSON to get status
var status = parseStatus(response.body());
if (!status.equals("Submitted")) {
return parseData(response.body());
}
System.out.println("Attempt " + attempt + ": Still processing...");
Thread.sleep(2000);
}
throw new Exception("Validation timeout");
}
var result = waitForValidation(documentId, 10);
System.out.println("Final status: " + result.get("status"));csharp
async Task<JsonElement> WaitForValidation(string documentId, int maxAttempts = 10) {
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
var response = await client.GetAsync($"/documents/{documentId}/status");
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
var data = json.GetProperty("data");
var status = data.GetProperty("status").GetString();
if (status != "Submitted") {
return data;
}
Console.WriteLine($"Attempt {attempt}: Still processing...");
await Task.Delay(2000);
}
throw new Exception("Validation timeout");
}
var result = await WaitForValidation(documentId);
Console.WriteLine($"Final status: {result.GetProperty("status").GetString()}");Bulk Submission
Submit multiple documents at once:
javascript
const documentIds = ['uuid-1', 'uuid-2', 'uuid-3'];
const result = await api('POST', '/documents/submit-bulk', {
document_ids: documentIds
});
console.log(`Submitted: ${result.data.submitted.length}`);
console.log(`Failed: ${result.data.failed.length}`);python
document_ids = ['uuid-1', 'uuid-2', 'uuid-3']
result = api('POST', '/documents/submit-bulk', {
'document_ids': document_ids
})
print(f"Submitted: {len(result['data']['submitted'])}")
print(f"Failed: {len(result['data']['failed'])}")php
$documentIds = ['uuid-1', 'uuid-2', 'uuid-3'];
$response = $api->post('/documents/submit-bulk', [
'document_ids' => $documentIds
]);
echo "Submitted: " . count($response->json('data.submitted')) . "\n";
echo "Failed: " . count($response->json('data.failed')) . "\n";php
$documentIds = ['uuid-1', 'uuid-2', 'uuid-3'];
$response = $client->post('documents/submit-bulk', [
'json' => ['document_ids' => $documentIds]
]);
$result = json_decode($response->getBody(), true)['data'];
echo "Submitted: " . count($result['submitted']) . "\n";
echo "Failed: " . count($result['failed']) . "\n";java
var documentIds = List.of("uuid-1", "uuid-2", "uuid-3");
var jsonBody = String.format("{\"document_ids\": %s}",
new ObjectMapper().writeValueAsString(documentIds));
var request = HttpRequest.newBuilder()
.uri(URI.create(API_URL + "/documents/submit-bulk"))
.header("Authorization", "Bearer " + API_TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
// Parse JSON to get submitted and failed counts
System.out.println("Result: " + response.body());csharp
var documentIds = new[] { "uuid-1", "uuid-2", "uuid-3" };
var response = await client.PostAsJsonAsync("/documents/submit-bulk", new {
document_ids = documentIds
});
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
var data = json.GetProperty("data");
Console.WriteLine($"Submitted: {data.GetProperty("submitted").GetArrayLength()}");
Console.WriteLine($"Failed: {data.GetProperty("failed").GetArrayLength()}");Handling Errors
Document Not Ready
json
{
"success": false,
"message": "Document must be validated and in 'ready' status before submission",
"error_code": "DOCUMENT_NOT_READY"
}Solution: Call validate first.
Quota Exceeded
json
{
"success": false,
"message": "Document quota exceeded. You have 0 documents remaining out of 100.",
"error_code": "QUOTA_EXCEEDED"
}Solution: Upgrade subscription or wait for quota reset.
LHDN Rejection
json
{
"success": false,
"message": "LHDN rejected the document",
"error_code": "LHDN_REJECTED",
"errors": {
"lhdn_error": {
"code": "ERR001",
"message": "Invalid TIN format"
}
}
}Solution: Fix the error and resubmit.
Error Handling Example
javascript
try {
const submission = await api('POST', `/documents/${documentId}/submit`);
console.log('Submitted successfully:', submission.data.document_uuid);
} catch (error) {
const errorCode = error.response?.data?.error_code;
switch (errorCode) {
case 'DOCUMENT_NOT_READY':
// Validate first
await api('POST', `/documents/${documentId}/validate`);
return api('POST', `/documents/${documentId}/submit`);
case 'QUOTA_EXCEEDED':
console.log('Please upgrade your subscription');
break;
case 'LHDN_REJECTED':
console.log('LHDN Error:', error.response?.data?.errors?.lhdn_error);
break;
default:
console.log('Submission failed:', error.message);
}
}python
try:
submission = api('POST', f'/documents/{document_id}/submit')
print(f"Submitted successfully: {submission['data']['document_uuid']}")
except Exception as e:
error_code = getattr(e, 'error_code', None)
if error_code == 'DOCUMENT_NOT_READY':
# Validate first
api('POST', f'/documents/{document_id}/validate')
submission = api('POST', f'/documents/{document_id}/submit')
elif error_code == 'QUOTA_EXCEEDED':
print('Please upgrade your subscription')
elif error_code == 'LHDN_REJECTED':
print(f"LHDN Error: {e.errors.get('lhdn_error')}")
else:
print(f'Submission failed: {e}')php
try {
$response = $api->post("/documents/$documentId/submit");
if ($response->successful()) {
echo "Submitted successfully: {$response->json('data.document_uuid')}\n";
} else {
$errorCode = $response->json('error_code');
switch ($errorCode) {
case 'DOCUMENT_NOT_READY':
// Validate first
$api->post("/documents/$documentId/validate");
$api->post("/documents/$documentId/submit");
break;
case 'QUOTA_EXCEEDED':
echo "Please upgrade your subscription\n";
break;
case 'LHDN_REJECTED':
print_r($response->json('errors.lhdn_error'));
break;
default:
echo "Submission failed: {$response->json('message')}\n";
}
}
} catch (Exception $e) {
echo "Error: {$e->getMessage()}\n";
}php
use GuzzleHttp\Exception\ClientException;
try {
$response = $client->post("documents/$documentId/submit");
$result = json_decode($response->getBody(), true);
echo "Submitted successfully: {$result['data']['document_uuid']}\n";
} catch (ClientException $e) {
$result = json_decode($e->getResponse()->getBody(), true);
$errorCode = $result['error_code'] ?? null;
switch ($errorCode) {
case 'DOCUMENT_NOT_READY':
// Validate first
$client->post("documents/$documentId/validate");
$client->post("documents/$documentId/submit");
break;
case 'QUOTA_EXCEEDED':
echo "Please upgrade your subscription\n";
break;
case 'LHDN_REJECTED':
print_r($result['errors']['lhdn_error']);
break;
default:
echo "Submission failed: {$result['message']}\n";
}
} catch (Exception $e) {
echo "Error: {$e->getMessage()}\n";
}java
try {
var request = HttpRequest.newBuilder()
.uri(URI.create(API_URL + "/documents/" + documentId + "/submit"))
.header("Authorization", "Bearer " + API_TOKEN)
.POST(HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
System.out.println("Submitted successfully");
} else {
// Parse error response and handle based on error_code
var errorCode = parseErrorCode(response.body());
switch (errorCode) {
case "DOCUMENT_NOT_READY":
// Validate first, then resubmit
break;
case "QUOTA_EXCEEDED":
System.out.println("Please upgrade your subscription");
break;
case "LHDN_REJECTED":
System.out.println("LHDN Error: " + parseLhdnError(response.body()));
break;
default:
System.out.println("Submission failed");
}
}
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}csharp
try {
var response = await client.PostAsync($"/documents/{documentId}/submit", null);
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
if (response.IsSuccessStatusCode) {
Console.WriteLine($"Submitted successfully: {json.GetProperty("data").GetProperty("document_uuid").GetString()}");
} else {
var errorCode = json.GetProperty("error_code").GetString();
switch (errorCode) {
case "DOCUMENT_NOT_READY":
// Validate first
await client.PostAsync($"/documents/{documentId}/validate", null);
await client.PostAsync($"/documents/{documentId}/submit", null);
break;
case "QUOTA_EXCEEDED":
Console.WriteLine("Please upgrade your subscription");
break;
case "LHDN_REJECTED":
Console.WriteLine($"LHDN Error: {json.GetProperty("errors").GetProperty("lhdn_error")}");
break;
default:
Console.WriteLine($"Submission failed: {json.GetProperty("message").GetString()}");
break;
}
}
} catch (Exception e) {
Console.WriteLine($"Error: {e.Message}");
}Next Steps
- QR Codes - Get verification QR codes for valid documents
- Error Codes - Full error reference
- Rate Limits - API rate limiting
