Appearance
Validation
Before submitting documents to LHDN, validate them to catch errors early.
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
Validation checks your document against LHDN requirements:
- Required fields are present
- Field formats are correct
- Tax calculations are valid
- Buyer/seller information is complete
Validating a Document
javascript
const validation = await api('POST', `/documents/${documentId}/validate`);
if (validation.data.validation_passed) {
console.log('Validation passed!');
console.log(`Status: ${validation.data.status}`); // 'ready'
} else {
console.log('Validation failed');
console.log('Errors:', validation.errors);
}python
validation = api('POST', f'/documents/{document_id}/validate')
if validation['data'].get('validation_passed'):
print('Validation passed!')
print(f"Status: {validation['data']['status']}") # 'ready'
else:
print('Validation failed')
print('Errors:', validation.get('errors'))php
$response = $api->post("/documents/$documentId/validate");
$result = $response->json('data');
if ($result['validation_passed'] ?? false) {
echo "Validation passed!\n";
echo "Status: {$result['status']}\n"; // 'ready'
} else {
echo "Validation failed\n";
print_r($response->json('errors'));
}php
$response = $client->post("documents/$documentId/validate");
$result = json_decode($response->getBody(), true);
if ($result['data']['validation_passed'] ?? false) {
echo "Validation passed!\n";
echo "Status: {$result['data']['status']}\n"; // 'ready'
} else {
echo "Validation failed\n";
print_r($result['errors']);
}java
var request = HttpRequest.newBuilder()
.uri(URI.create(API_URL + "/documents/" + documentId + "/validate"))
.header("Authorization", "Bearer " + API_TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
// Parse JSON response and check validation_passed
System.out.println("Validation response: " + response.body());csharp
var response = await client.PostAsync($"/documents/{documentId}/validate", null);
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
if (json.GetProperty("data").GetProperty("validation_passed").GetBoolean()) {
Console.WriteLine("Validation passed!");
Console.WriteLine($"Status: {json.GetProperty("data").GetProperty("status").GetString()}");
} else {
Console.WriteLine("Validation failed");
Console.WriteLine($"Errors: {json.GetProperty("errors")}");
}Success Response
json
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "ready",
"validation_passed": true,
"message": "Document validated successfully and marked as ready for submission"
}
}After successful validation:
- Document status changes from
drafttoready - Document is ready for LHDN submission
Failure Response
json
{
"success": false,
"message": "Validation failed",
"error_code": "VALIDATION_FAILED",
"errors": {
"validation_errors": [
{
"field": "buyer.tin",
"message": "Buyer TIN is required for LHDN submission"
},
{
"field": "invoiceLines.0.classifications",
"message": "At least one classification code is required"
}
]
}
}Common Validation Errors
| Field | Error | Solution |
|---|---|---|
buyer.tin | TIN is required | Add TIN to buyer |
buyer.id_type | ID type required | Add BRN, NRIC, PASSPORT, or ARMY |
buyer.address | Address incomplete | Add all required address fields |
invoiceLines | Lines required | Add at least one line item |
invoiceLines.*.classifications | Classification required | Add product classification code |
invoiceLines.*.invoiceLineTaxes | Tax required | Add at least one tax entry |
documentReferences | References required | Required for credit/debit/refund notes |
Validation Workflow
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Create │ ──▶ │ Validate │ ──▶ │ Submit │
│ (draft) │ │ (ready) │ │ (Submitted) │
└─────────────┘ └─────────────┘ └─────────────┘
│
│ Failed
▼
┌─────────────┐
│ Fix Errors │
└─────────────┘Handling Validation Errors
javascript
try {
const validation = await api('POST', `/documents/${documentId}/validate`);
if (!validation.data.validation_passed) {
// Handle specific errors
for (const error of validation.errors?.validation_errors || []) {
console.log(`${error.field}: ${error.message}`);
// Fix the error based on field
if (error.field === 'buyer.tin') {
// Update buyer with TIN
await api('PUT', `/buyers/${buyerId}`, { tin: 'C12345678000' });
}
}
// Re-validate after fixes
return api('POST', `/documents/${documentId}/validate`);
}
} catch (error) {
console.error('Validation error:', error);
}python
try:
validation = api('POST', f'/documents/{document_id}/validate')
if not validation['data'].get('validation_passed'):
# Handle specific errors
for error in validation.get('errors', {}).get('validation_errors', []):
print(f"{error['field']}: {error['message']}")
# Fix the error based on field
if error['field'] == 'buyer.tin':
# Update buyer with TIN
api('PUT', f'/buyers/{buyer_id}', {'tin': 'C12345678000'})
# Re-validate after fixes
validation = api('POST', f'/documents/{document_id}/validate')
except Exception as e:
print(f'Validation error: {e}')php
try {
$response = $api->post("/documents/$documentId/validate");
$result = $response->json('data');
if (!($result['validation_passed'] ?? false)) {
// Handle specific errors
foreach ($response->json('errors.validation_errors', []) as $error) {
echo "{$error['field']}: {$error['message']}\n";
// Fix the error based on field
if ($error['field'] === 'buyer.tin') {
// Update buyer with TIN
$api->put("/buyers/$buyerId", ['tin' => 'C12345678000']);
}
}
// Re-validate after fixes
$response = $api->post("/documents/$documentId/validate");
}
} catch (Exception $e) {
echo "Validation error: {$e->getMessage()}\n";
}php
try {
$response = $client->post("documents/$documentId/validate");
$result = json_decode($response->getBody(), true);
if (!($result['data']['validation_passed'] ?? false)) {
// Handle specific errors
foreach ($result['errors']['validation_errors'] ?? [] as $error) {
echo "{$error['field']}: {$error['message']}\n";
// Fix the error based on field
if ($error['field'] === 'buyer.tin') {
// Update buyer with TIN
$client->put("buyers/$buyerId", [
'json' => ['tin' => 'C12345678000']
]);
}
}
// Re-validate after fixes
$response = $client->post("documents/$documentId/validate");
}
} catch (Exception $e) {
echo "Validation error: {$e->getMessage()}\n";
}java
try {
var request = HttpRequest.newBuilder()
.uri(URI.create(API_URL + "/documents/" + documentId + "/validate"))
.header("Authorization", "Bearer " + API_TOKEN)
.POST(HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
// Parse JSON and check validation_passed
// If failed, iterate through errors and fix them
// Example: Update buyer with TIN if that field failed
var updateRequest = HttpRequest.newBuilder()
.uri(URI.create(API_URL + "/buyers/" + buyerId))
.header("Authorization", "Bearer " + API_TOKEN)
.header("Content-Type", "application/json")
.PUT(HttpRequest.BodyPublishers.ofString("{\"tin\": \"C12345678000\"}"))
.build();
client.send(updateRequest, HttpResponse.BodyHandlers.ofString());
// Re-validate after fixes
client.send(request, HttpResponse.BodyHandlers.ofString());
} catch (Exception e) {
System.err.println("Validation error: " + e.getMessage());
}csharp
try {
var response = await client.PostAsync($"/documents/{documentId}/validate", null);
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
if (!json.GetProperty("data").GetProperty("validation_passed").GetBoolean()) {
// Handle specific errors
var errors = json.GetProperty("errors").GetProperty("validation_errors");
foreach (var error in errors.EnumerateArray()) {
var field = error.GetProperty("field").GetString();
var message = error.GetProperty("message").GetString();
Console.WriteLine($"{field}: {message}");
// Fix the error based on field
if (field == "buyer.tin") {
// Update buyer with TIN
await client.PutAsJsonAsync($"/buyers/{buyerId}", new { tin = "C12345678000" });
}
}
// Re-validate after fixes
response = await client.PostAsync($"/documents/{documentId}/validate", null);
}
} catch (Exception e) {
Console.WriteLine($"Validation error: {e.Message}");
}Pre-Submission Checklist
Before calling validate, ensure:
- [ ] Buyer has TIN, ID type, and ID value
- [ ] Buyer address has all required fields
- [ ] Invoice has at least one line item
- [ ] Each line has classification, description, quantity, unit code, unit price
- [ ] Each line has at least one tax entry
- [ ] Credit/debit/refund notes have document references
- [ ] Self-billed documents use
onBehalfSellerId
Batch Validation
While there's no batch validate endpoint, you can validate multiple documents in parallel:
javascript
const documentIds = ['uuid-1', 'uuid-2', 'uuid-3'];
const results = await Promise.all(
documentIds.map(id => api('POST', `/documents/${id}/validate`))
);
results.forEach((result, index) => {
console.log(`Document ${documentIds[index]}: ${result.data.status}`);
});python
import asyncio
import aiohttp
document_ids = ['uuid-1', 'uuid-2', 'uuid-3']
async def validate_document(session, doc_id):
async with session.post(f'{API_URL}/documents/{doc_id}/validate') as response:
return await response.json()
async def validate_all():
async with aiohttp.ClientSession(headers={
'Authorization': f'Bearer {API_TOKEN}',
'Accept': 'application/json'
}) as session:
tasks = [validate_document(session, id) for id in document_ids]
return await asyncio.gather(*tasks)
results = asyncio.run(validate_all())
for doc_id, result in zip(document_ids, results):
print(f"Document {doc_id}: {result['data']['status']}")php
$documentIds = ['uuid-1', 'uuid-2', 'uuid-3'];
$results = collect($documentIds)->map(function ($id) use ($api) {
return $api->post("/documents/$id/validate")->json('data');
});
$results->each(function ($result, $index) use ($documentIds) {
echo "Document {$documentIds[$index]}: {$result['status']}\n";
});php
use GuzzleHttp\Promise\Utils;
$documentIds = ['uuid-1', 'uuid-2', 'uuid-3'];
$promises = [];
foreach ($documentIds as $id) {
$promises[$id] = $client->postAsync("/documents/$id/validate");
}
$results = Utils::settle($promises)->wait();
foreach ($results as $id => $result) {
if ($result['state'] === 'fulfilled') {
$data = json_decode($result['value']->getBody(), true);
echo "Document $id: {$data['data']['status']}\n";
}
}java
import java.util.concurrent.CompletableFuture;
import java.util.List;
List<String> documentIds = List.of("uuid-1", "uuid-2", "uuid-3");
List<CompletableFuture<HttpResponse<String>>> futures = documentIds.stream()
.map(id -> {
var request = HttpRequest.newBuilder()
.uri(URI.create(API_URL + "/documents/" + id + "/validate"))
.header("Authorization", "Bearer " + API_TOKEN)
.POST(HttpRequest.BodyPublishers.noBody())
.build();
return client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
})
.toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
for (int i = 0; i < documentIds.size(); i++) {
var response = futures.get(i).join();
System.out.println("Document " + documentIds.get(i) + ": " + response.body());
}csharp
var documentIds = new[] { "uuid-1", "uuid-2", "uuid-3" };
var tasks = documentIds.Select(async id => {
var response = await client.PostAsync($"/documents/{id}/validate", null);
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
return new { Id = id, Status = json.GetProperty("data").GetProperty("status").GetString() };
});
var results = await Task.WhenAll(tasks);
foreach (var result in results) {
Console.WriteLine($"Document {result.Id}: {result.Status}");
}Next Steps
- Submission - Submit validated documents to LHDN
- QR Codes - Get verification QR codes
- Error Codes - Full error reference
