Skip to content

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:

  1. Submit the document to LHDN
  2. Check status to confirm acceptance

Prerequisites

Before submitting:

  • Document must be in ready status (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

StatusDescription
SubmittedSent to LHDN, awaiting validation
ValidAccepted by LHDN
InvalidRejected 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

InvoisX - Malaysia's Leading e-Invoice Platform