Skip to content

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 draft to ready
  • 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

FieldErrorSolution
buyer.tinTIN is requiredAdd TIN to buyer
buyer.id_typeID type requiredAdd BRN, NRIC, PASSPORT, or ARMY
buyer.addressAddress incompleteAdd all required address fields
invoiceLinesLines requiredAdd at least one line item
invoiceLines.*.classificationsClassification requiredAdd product classification code
invoiceLines.*.invoiceLineTaxesTax requiredAdd at least one tax entry
documentReferencesReferences requiredRequired 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

InvoisX - Malaysia's Leading e-Invoice Platform