SpainNotebook64

July 10, 2026 · View on GitHub

Maven Central License: MIT Kotlin

Kotlin library to read and decode the barcodes printed on Spanish financial documents — the CECA/AEB Cuaderno 64 standard (June 2016). Given a barcode payload string it auto-detects the document format (tipo) and extracts every field with its name, value, position and description.

Pure Kotlin/JVM, zero runtime dependencies.

Installation

Gradle (Kotlin DSL):

dependencies {
    implementation("io.github.joyner-perez:SpainNotebook64:1.0.2")
}

Gradle (Groovy):

implementation 'io.github.joyner-perez:SpainNotebook64:1.0.2'

Version catalog (gradle/libs.versions.toml):

[versions]
spainNotebook64 = "1.0.2"

[libraries]
spain-notebook64 = { module = "io.github.joyner-perez:SpainNotebook64", version.ref = "spainNotebook64" }

Then reference it in build.gradle.kts:

dependencies {
    implementation(libs.spain.notebook64)
}

Maven:

<dependency>
    <groupId>io.github.joyner-perez</groupId>
    <artifactId>SpainNotebook64</artifactId>
    <version>1.0.2</version>
</dependency>

Usage

The whole public API is two top-level functions in the com.joyner.notebook64 package.

Parse a barcode

import com.joyner.notebook64.parse

// Scanned barcode payload (tipo 534). The GS1-128 AI(90) prefix is optional.
val result = parse("5342024000123456    0012345612345678AJLGR280001")

println(result.tipo)                       // "534"
println(result.value("numeroJustificante"))// "2024000123456"
println(result.value("importe"))           // "00123456"
println(result.value("nif"))               // "12345678A"

The GS1-128 Application Identifier prefix is accepted in both forms and stripped automatically:

parse("(90)5342024000123456    0012345612345678AJLGR280001") // parenthesised AI
parse("905342024000123456    0012345612345678AJLGR280001")   // bare AI
parse("5342024000123456    0012345612345678AJLGR280001")     // no AI

Force a specific format

By default the tipo is auto-detected from the embedded indicator field. Pass it explicitly to skip detection (and to validate the input against that format's expected length):

val result = parse(input = "50128123015...", tipo = "501")

List supported formats

import com.joyner.notebook64.availableFormats

availableFormats().forEach { format ->
    println("${format.tipo} — ${format.description}")
}
// 501 — Tributos Administración Local
// 502 — Tributos y otros ingresos municipales - formato corto modalidad 1
// ...

Interpreting the result

parse(...) returns a Notebook64Result:

PropertyTypeDescription
tipoStringDetected (or forced) 3-digit format code, e.g. "534".
rawInputStringThe payload that was parsed (after stripping any GS1-128 AI prefix).
fieldsList<ParsedField>Every field extracted from the payload, in barcode order.

Each ParsedField describes one segment of the barcode:

PropertyTypeDescription
nameStringField identifier, e.g. "importe", "nif", "numeroJustificante".
valueStringRaw characters of the field exactly as they appear in the barcode.
startPosInt1-indexed start position within the full barcode (AI prefix included).
lengthIntNumber of characters in the field.
descriptionStringHuman-readable description of the field (in Spanish).

Convenience accessors

val result = parse("5342024000123456    0012345612345678AJLGR280001")

// Look up the whole field by name (null if absent):
val importe: ParsedField? = result.field("importe")

// Or just the value (null if absent):
val nif: String? = result.value("nif")

// Or iterate everything:
result.fields.forEach { f ->
    println("${f.name} = '${f.value}'  (pos ${f.startPos}, len ${f.length}) — ${f.description}")
}

Reading values correctly

  • Values are raw strings, never converted. Leading zeros, padding spaces and check digits are preserved as printed. Convert yourself when needed.
  • Amounts (importe) are integers in céntimos with two implicit decimals. Divide by 100 to get euros — e.g. "00123456"1234.56 €.
  • Alphanumeric fields (NIF, anagrama, concepto…) may be space-padded to a fixed width; trim if your use case requires it.
  • applicationIdentifier — when the input carries a GS1-128 AI prefix, the first field is the AI itself ("90" or "21"); subsequent field startPos values account for that offset.

Errors

parse(...) throws IllegalArgumentException when:

  • an explicit tipo is not a registered format, or
  • no tipo can be auto-detected from the input, or
  • the input length does not match the expected length for the resolved format.
try {
    parse(rawScan)
} catch (e: IllegalArgumentException) {
    // e.g. "No se puede detectar el tipo. Longitud=30, primeros 3 chars='999'"
    println("Invalid barcode: ${e.message}")
}

License

MIT © Joyner Pérez Echevarría