A Kotlin Multiplatform client library for the e-invoice API of the Philippine Bureau of Internal Revenue's Electronic Invoicing System (EIS). It covers the three calls a seller's system makes (authentication, invoice issuance, result inquiry), the CAS e-invoice JSON model, a validator that checks a payload the way EIS does, and the transport crypto from BIR's API guide.
It also ships bir-einvoice-mock, a local stand-in for EIS, so you can test the whole flow
before you have sandbox credentials.
Not affiliated with or endorsed by the BIR. This is independent open-source software by JE Ramos (EveryTech). It is not accredited, certified, or endorsed by the Bureau of Internal Revenue. The BIR's own documents are the authority. A payload that passes the mock or the validator here can still be rejected by the BIR sandbox, and the sandbox has the final word.
Status: 0.1.0, in preparation. No version has been released or tagged yet, so for now you build it from source (see Install). JVM target (JDK 21), plus Kotlin/Wasm for the parts that run in a browser (see Platforms). The source sets are laid out so Android and iOS targets can be added later. The crypto follows BIR's Java, Python and C# samples. The client and the mock have been tested in both directions against an independent implementation of the same protocol, which is not published. Nobody has run this library against the live BIR sandbox yet.
| Module | Coordinates | Contents |
|---|---|---|
bir-einvoice |
io.everytech:bir-einvoice:0.1.0 |
CasInvoice model, Amount, EisJson, CasValidator, EisCodes, EisUrls, EisCrypto, EisIds, EisClient |
bir-einvoice-mock |
io.everytech:bir-einvoice-mock:0.1.0 |
MockEisServer for tests, plus a standalone runner; MockEis, the same processing without the HTTP and crypto layers |
Dependencies: the Kotlin standard library and kotlinx-serialization-json. HTTP uses the JDK's
java.net.http.HttpClient; the mock uses the JDK's built-in com.sun.net.httpserver.
| JVM | Kotlin/Wasm (wasmJs, browsers) |
|
|---|---|---|
CasInvoice, Amount, EisJson, CasValidator, EisCodes, EisUrls, EisIds |
yes | yes |
EisCrypto, EisClient, JdkEisTransport |
yes | no |
MockEis (in-process registration, results and checks) |
yes | yes |
MockEisServer, the standalone runner |
yes | no |
The transport stays on the JVM on purpose: a browser cannot call the EIS (no CORS, and the EIS
expects a fixed, registered IP address), and the signing key should not live in a web page.
In a browser, MockEis gives a practice run that never leaves the page. EisIds uses
SecureRandom on the JVM and crypto.getRandomValues in a browser. The common tests run on
both (./gradlew check runs the Wasm ones on Node.js, which Gradle downloads).
The library is not on Maven Central. Until v0.1.0 is tagged, build it from source with one of the first two routes below.
Clone this repository next to your project and include it. Gradle swaps the coordinates for the source build.
// settings.gradle.kts
includeBuild("../bir-einvoice")
// build.gradle.kts
dependencies {
implementation("io.everytech:bir-einvoice:0.1.0")
testImplementation("io.everytech:bir-einvoice-mock:0.1.0")
}Run ./gradlew publishToMavenLocal in this repository, add mavenLocal() to your repositories,
and use the same io.everytech coordinates as above.
No tag exists yet, so this does not work today. Once a release such as v0.1.0 is tagged, JitPack
builds it on the first request (see jitpack.yml):
repositories {
maven("https://jitpack.io")
}
dependencies {
implementation("com.github.everytech-io:bir-einvoice:<tag>")
}JitPack serves the individual modules as com.github.everytech-io.bir-einvoice:bir-einvoice:<tag>
and com.github.everytech-io.bir-einvoice:bir-einvoice-mock:<tag>.
Everything comes from the EIS Certification Portal, under the account of the taxpayer who issues the invoices.
| Field | Where it comes from |
|---|---|
eisCertNumber |
The EIS Cert Number (formerly Accreditation ID) on the application's EIS Cert page. It is sent as the accreditationId header. |
applicationId, applicationKey |
The application's Setting page. The key signs the authentication call only. |
userId, password |
Your EIS Certification Portal login. |
eisPublicKey |
The EIS Certification Public Key from the portal's Downloads page, as base64 SPKI or PEM. |
signingKeyId, signingPrivateKey |
Your own RSA signing key pair and its key ID. The private key is PKCS#8, base64 DER or PEM. The key ID goes in each invoice's JWS kid. |
baseUrl |
Defaults to the sandbox, https://eis-cert.bir.gov.ph. Take the production URL from the portal once your PTT is issued. It must start with https://. http:// is accepted only for a test server on your own computer (localhost, 127.0.0.1 or [::1]), such as MockEisServer. |
EisClient throws IllegalArgumentException for a base URL that breaks that rule.
EisUrls.checkBaseUrl(url) applies the same rule and returns the reason, or null when the URL is
fine, so an application can check a URL before it saves it.
Keep these out of source control. EisCredentials.toString() never prints a secret, and
EisCredentials.toJson() / fromJson() read and write a plain JSON file you can store with
owner-only permissions.
The sandbox allows one call per 10 seconds and answers faster callers with error E19. When the
base URL is the sandbox, the client waits 10.5 seconds between calls. For any other base URL it
does not wait. Pass sandboxSpacingMillis to change either.
import io.everytech.bireinvoice.*
import java.io.File
val credentials = EisCredentials.fromJson(File("eis-credentials.json").readText())
val client = EisClient(credentials)
val issueDate = "2026-10-29"
val invoice = CasInvoice(
compInvoiceId = "INV-00000001",
issueDtm = "20261029",
eisUniqueId = EisIds.eisUniqueId(issueDate, credentials.eisCertNumber),
docType = EisCodes.DOC_INVOICE,
transClass = EisCodes.TRANS_VATABLE,
corrYN = "N",
sellerInfo = SellerInfo(
tin = "123456789", branchCd = "00000", type = EisCodes.SELLER_VAT,
regNm = "SAMPLE TRADING CORP.", regAddr = "1 Ayala Ave, Makati City",
),
buyerInfo = BuyerInfo(tin = "987654321", branchCd = "00000", regNm = "BUYER INC."),
itemList = listOf(
CasItem(
nm = "Widget", desc = "Blue", qty = Amount.parse("3"), unit = "pc",
unitCost = Amount.parse("1000.00"), salesAmt = Amount.parse("3000.00"),
regDscntAmt = Amount.ZERO, speDscntAmt = Amount.ZERO, netSales = Amount.parse("3000.00"),
),
),
totNetItemSales = Amount.parse("3000.00"),
discount = Discount(Amount.ZERO, Amount.ZERO, Amount.ZERO, Amount.ZERO),
otherTaxRev = Amount.ZERO,
totNetSalesAftDisct = Amount.parse("3000.00"),
vatAmt = Amount.parse("360.00"),
withholdIncome = Amount.ZERO,
withholdBusVat = Amount.ZERO,
withholdBusPt = Amount.ZERO,
otherNonTaxCharge = Amount.ZERO,
netAmtPay = Amount.parse("3360.00"),
ptuNum = "AC-0001",
)
val json = EisJson.encode(invoice) // spec field order, amounts like 3360.00
val problems = CasValidator.validate(json) // [] when it passes
require(problems.isEmpty()) { problems.joinToString("\n") }
val ack = client.submit(listOf(json)) // signs, encrypts, sends; 1 to 100 per call
var result = client.inquire(ack.submitId)
while (result.processStatusCode == EisCodes.PROCESS_IN_PROGRESS) {
Thread.sleep(60_000)
result = client.inquire(ack.submitId)
}
result.processedDocuments.forEach { println("${it.invoiceUid}: ${it.resultStatusCode} ${it.description ?: ""}") }Calls block. Run them off the UI thread, for example in withContext(Dispatchers.IO).
Money is never a floating-point number here. Amount holds integer hundredths and writes exactly
two decimals. A credit memo uses DocType 03, CorrYN Y, a correction code from
EisCodes.correctionCodes, the corrected invoice's EisUniqueId in PrevUniqueId (or
EisCodes.UNMANAGED_ORIGINAL when the original was never in EIS), and negative amounts.
The client, crypto, IDs and validator work on JSON text and are plain Java calls. EisException
is a checked exception.
import io.everytech.bireinvoice.*;
import java.nio.file.*;
import java.util.List;
EisCredentials credentials = EisCredentials.fromJson(Files.readString(Path.of("eis-credentials.json")));
EisClient client = new EisClient(credentials);
String json = Files.readString(Path.of("invoice.json"));
List<SchemaProblem> problems = CasValidator.validate(json);
if (!problems.isEmpty()) throw new IllegalStateException(problems.toString());
try {
SubmitAck ack = client.submit(List.of(json));
InquiryResult result = client.inquire(ack.getSubmitId());
System.out.println(result.getProcessStatusCode());
} catch (EisException e) {
System.err.println(e.getCode() + " " + EisCodes.getRequestErrors().get(e.getCode()));
}Java can also build the typed model and let the library write the JSON:
Amount zero = new Amount(0L); // integer hundredths: new Amount(85000L) is 850.00
CasItem item = new CasItem("Widget", "Blue", new Amount(300L), "pc", new Amount(100_000L),
new Amount(300_000L), zero, zero, new Amount(300_000L));
CasInvoice invoice = new CasInvoice("INV-00000001", "20261029", uid, EisCodes.DOC_INVOICE,
EisCodes.TRANS_VATABLE, "N", "", "", "",
new SellerInfo("123456789", "00000", EisCodes.SELLER_VAT, "SAMPLE TRADING CORP.", "", "", "Makati City"),
new BuyerInfo("987654321", "00000", "BUYER INC.", "", "", "", "", "", "", "", ""),
List.of(item), new Amount(300_000L), new Discount(zero, zero, zero, zero, ""), zero,
new Amount(300_000L), new Amount(36_000L), zero, zero, zero, zero, new Amount(336_000L), null, "AC-0001");
String json = EisJson.encode(invoice); // "VATAmt":360.00MockEisServer(processingPolls = 1).use { mock ->
mock.start()
val client = EisClient(mock.credentials, sandboxSpacingMillis = 0)
val ack = client.submit(listOf(json))
client.inquire(ack.submitId) // processStatusCode 02 (in progress)
client.inquire(ack.submitId) // 01, with SUC001 or the error code per invoice
}The mock generates its own keys and credentials on each start. It checks the headers, the clock (E05), the HMAC (E04), the RSA and AES layers (E21, E22), the portal login (E23), the token (E03), submit IDs (E11, E14, E15), JWS signatures and key ID (SYN002), the schema and formulas (SYN004, ERR002 to ERR014), duplicates (SYN003), the EIS Cert Number inside the ID (ERR006), and that a credit memo's original was registered (ERR007).
To test a client written in another language, run the mock on its own. It writes a credentials file and keeps running until stopped:
MOCK_EIS_PORT=8787 ./gradlew :bir-einvoice-mock:run --args="mock-eis-credentials.json"| Step | Algorithm |
|---|---|
Authentication request data |
RSA/ECB/PKCS1Padding with the EIS public key, over {"userId","password","authKey"} |
authKey |
32 random characters from 0-9A-Za-z@#$%^&*() (SecureRandom) |
Authentication response, submit request and submit response data |
AES/CBC/PKCS5Padding, key = the 32 UTF-8 bytes of the key, IV = its first 16 bytes, base64 |
authorization header |
Bearer + base64(HmacSHA256(key, datetime + METHOD + path)); the application key for authentication, the session key otherwise |
datetime header |
yyyyMMddHHmmss in Philippine time (UTC+8) |
| Invoice signature | compact JWS, RS256, header {"kid":...,"alg":"RS256"}, base64url without padding |
BIR's protocol derives the IV from the key. That is BIR's choice, so use these keys for EIS and nothing else.
Request errors come back as EisException.code. The client also uses HTTP (a response that
is not EIS JSON), HTTP<status> (an error with no code), NETWORK (no response) and RESPONSE
(a response it could not decrypt or read).
| Code | Meaning and fix |
|---|---|
| E01 | EIS Cert Number not recognised, or not registered for production |
| E02 | Application ID not recognised for this EIS Cert Number |
| E03 | Auth token expired (6 hours) or invalid. The client signs in again and retries once, with the same submit ID |
| E04 | HMAC wrong: application key for authentication, session key for other calls |
| E05 | datetime is not current Philippine time, is in the wrong format, or is too far off |
| E06 | EIS key pair expired (BIR: does not occur) |
| E07 | Production call from an IP that is not whitelisted (4 IPs per application) |
| E08 | Sandbox locked once the EIS certificate is requested. Create a new application to keep testing |
| E11 | Request body JSON does not match the guide |
| E12, E13 | Invalid or expired eisKeyId (BIR: does not occur) |
| E14 | Duplicate submit ID |
| E15 | Submit ID unknown, expired, or not yours |
| E16 | No result document (BIR: does not occur) |
| E17 | Callback URL not responding |
| E18 | Callback returned the wrong response shape |
| E19 | Sandbox called more than once per 10 seconds |
| E21 | data could not be decrypted: wrong key or algorithm |
| E22 | data decrypted but is not valid JSON |
| E23 | Wrong EIS Certification Portal user ID or password |
| E24 | authKey is not a 32-character string |
| E31 | Invalid callback response body (BIR: does not occur) |
| E99 | EIS internal error. Send the full request headers and body to the EIS team |
Per-invoice results come back in InquiryResult.processedDocuments[].resultStatusCode:
| Code | Meaning |
|---|---|
| SUC001 | Passed |
| SYN002 | Invalid digital signature |
| SYN003 | Duplicate EisUniqueId |
| SYN004 | Schema error: missing or extra fields, codes, types, number format |
| ERR001 | Seller TIN not registered |
| ERR002 | Issue date invalid, after transmission, or different from the date inside EisUniqueId |
| ERR003 | Transmission date invalid |
| ERR004 | Sales and VAT have different signs, or VAT is not 0 on zero-rated or exempt sales |
| ERR005 | Classification code error |
| ERR006 | EIS Cert Number inside EisUniqueId differs from the sender's |
| ERR007 | PrevUniqueId missing or not the seller's, or present on a document that is not a correction |
| ERR011 | Invalid document type |
| ERR012 | Invalid seller type |
| ERR013 | RBE local sale from a seller that is not an RBE |
| ERR014 | Invalid transaction classification |
| ERR015 | TIN and branch code are not an RBE |
| ERR999 | Other |
Both tables are also in code: EisCodes.requestErrors and EisCodes.resultCodes.
- EIS e-invoice API Development Guide v2.3 and e-invoice JSON File Format v2.1 (CAS sheet v2.2): https://eis-cert.bir.gov.ph/#/downloads/1331
- Revenue Memorandum Circular No. 98-2026: https://bir-cdn.bir.gov.ph/BIR/pdf/RMC%20No.%2098-2026%20redacted.pdf
- EIS Certification Portal downloads (public key, API Error Solution Table, sample code): https://eis-cert.bir.gov.ph/#/downloads
This repository links to BIR documents and does not redistribute them. docs/EIS-API-SPEC.md is this project's own digest of them and docs/eis-api.openapi.yaml an OpenAPI description written from the guide. When they disagree with BIR's documents, BIR's documents win.
Open an issue with the
bug report form.
It asks for the library version, your Kotlin or Java version, the EIS error code, and a minimal
code snippet. A snippet that fails against MockEisServer is the quickest to fix.
Issues are public. Leave out TINs, customer details, real invoices, EIS credentials, keys and tokens; use made-up values. Security problems never go in a public issue: see SECURITY.md.
Problems in EveryInvoice, a free e-invoicing app by EveryTech built on this library (https://everyinvoice.everytech.io), do not go here: email contact@everytech.io.
./gradlew check # all tests
./gradlew publishToMavenLocal # io.everytech:bir-einvoice:0.1.0 and bir-einvoice-mock:0.1.0Two optional interop tests test this library against an independent implementation of the same protocol, written for Node.js, which is not published. They are skipped unless one of these variables is set, and CI does not run them:
BIR_EINVOICE_NODE_MOCK_CREDS: the credentials file of that implementation's running mock. The Kotlin client is tested against it.BIR_EINVOICE_NODE_DIR: the folder of that implementation. Its client is tested againstMockEisServer.
Contributions are welcome: bug reports, tests, documentation, and fixes that follow the BIR's published specifications. Every rule in the code must cite its official BIR source. The project is maintained by one person, JE Ramos, who does business as EveryTech.
- CONTRIBUTING.md: scope, setup, tests, commit style and the DCO sign-off
- CODE_OF_CONDUCT.md: Contributor Covenant 2.1
- SECURITY.md: report vulnerabilities privately
- SUPPORT.md: where to ask questions
- GOVERNANCE.md: maintainers and how decisions are made
- CHANGELOG.md: what changed in each version
- TRADEMARKS.md: use of the EveryTech name and logo
bir-einvoice is independent open-source software. It is not affiliated with or endorsed by the Bureau of Internal Revenue.
Apache License 2.0. See LICENSE and NOTICE. Copyright 2026 JE Ramos (EveryTech). "EveryTech" is the trade name JE Ramos uses; the license does not grant rights to it (see TRADEMARKS.md).