Skip to content

CFONB 120 statements

CFONB 120 is the French bank statement format (relevé de compte, also known as ETEBAC 3): one record of 120 characters per line, its first two characters being the record code.

  1. 01opening balance
  2. 04operation, repeated
  3. 05detail of the operation above, optional
  4. 07closing balance
One statement: its records, in the order of the file.
Record Parsed into Reached with
01 Balance $statement->getOldBalance()
04 Operation $statement->getOperations()
05 OperationDetail $operation->getDetails()
07 Balance $statement->getNewBalance()

Cfonb120Reader::parse() takes the contents of the file, not its path, and returns a list of Statement:

use Silarhi\Cfonb\Cfonb120Reader;
$reader = new Cfonb120Reader();
$content = file_get_contents('/path/to/cfonb120-file.txt');
foreach ($reader->parse($content) as $statement) {
// Process each statement
echo "Statement date: " . $statement->getNewBalance()->getDate()->format('Y-m-d') . "\n";
}

The examples of this page parse this file:

statements.txt
0110278 02204EUR2 00012345603 060420 0000000166956E060420070420
0410556677202204EUR2 00012345603B1070420 070420PRLV SEPA ONLINE SAS 0000000000000000000227R
0510556677202204EUR2 00012345603B1070420 LIBDEDIBOX 3706114
0710278 02204EUR2 00012345603 070420 0000000166728F
0110278 02204EUR2 00020427603 070420 0000000166728F070420080420
0410556677202204EUR2 00020427603B1080420 080420PRLV SEPA FREE MOBILE 0000000000000000000201J
0410278000202204EUR2 0002042760305080420 080420VIR JOHNDOE / FOOBAR 0000000000000000050000{
0410278085902204EUR2 0002042760306080420 080420VIR BOR TEST / FOOBAR 0000000000000000000201J01/01/2025.2
0710278 02204EUR2 00020427603 080420 0000000216326D
0110278 02204EUR2 00020427603 080420 0000000216326D080420090420
0710278 02204EUR2 00020427603 090420 0000000216326D

The reader goes through the lines in order:

  • an old balance line (01) opens a statement.
  • an operation line (04) is added to the open statement.
  • a detail line (05) is added to the last operation read.
  • a new balance line (07) closes the open statement, which is then added to the list: the next operation line needs a new 01 line.
  • an empty line is ignored; a line of spaces or tabs throws a ParseException.

Any other order throws a ParseException, in strict and non-strict mode alike: a 07 line, an operation or a detail line with no open statement, a 01 line while a statement is still open, a statement still open at the end of the file (a truncated file).

cfonb120-sequence.php
<?php
use Silarhi\Cfonb\Cfonb120Reader;
use Silarhi\Cfonb\Exceptions\ParseException;
$reader = new Cfonb120Reader();
$lines = file('statements.txt', FILE_IGNORE_NEW_LINES);
// A truncated file: the "07" new balance of the last statement is missing
try {
$reader->parse(implode("\n", array_slice($lines, 0, -1)));
} catch (ParseException $e) {
echo $e->getMessage(), "\n";
}
// A "04" operation before the "01" old balance of its statement
try {
$reader->parse(implode("\n", [$lines[1], $lines[0], $lines[3]]));
} catch (ParseException $e) {
echo $e->getMessage(), "\n";
}
Output
Unable to close the statement of account 00020427603 opened on 2020-04-08: missing "07" new balance record at the end of the file
Unable to attach the operation with code B1 dated 2020-04-07: missing "01" old balance record before it

So every statement returned has both balances: hasOldBalance() and hasNewBalance() are always true on them, and getOldBalance() and getNewBalance() never throw. A bank usually sends one statement per account and per day: a file can hold several days and several accounts, in the order the bank wrote them.

cfonb120-operation.php
<?php
use Silarhi\Cfonb\Cfonb120Reader;
$reader = new Cfonb120Reader();
$statements = $reader->parse(file_get_contents('statements.txt'));
// The first operation of the file, a SEPA direct debit with one detail line
$operation = $statements[0]->getOperations()[0];
var_dump([
'bankCode' => $operation->getBankCode(),
'deskCode' => $operation->getDeskCode(),
'accountNumber' => $operation->getAccountNumber(),
'internalCode' => $operation->getInternalCode(),
'currencyCode' => $operation->getCurrencyCode(),
'code' => $operation->getCode(),
'date' => $operation->getDate()->format('Y-m-d'),
'valueDate' => $operation->getValueDate()->format('Y-m-d'),
'rejectCode' => $operation->getRejectCode(),
'label' => $operation->getLabel(),
'reference' => $operation->getReference(),
'exemptCode' => $operation->getExemptCode(),
'amount' => $operation->getAmount(),
'bankOperationReference' => $operation->getBankOperationReference(),
]);
foreach ($operation->getDetails() as $detail) {
echo sprintf("Detail %s: %s\n", $detail->getQualifier(), $detail->getAdditionalInformations());
}
Output
array(14) {
["bankCode"]=>
string(5) "10556"
["deskCode"]=>
string(5) "02204"
["accountNumber"]=>
string(11) "00012345603"
["internalCode"]=>
string(4) "6772"
["currencyCode"]=>
string(3) "EUR"
["code"]=>
string(2) "B1"
["date"]=>
string(10) "2020-04-07"
["valueDate"]=>
string(10) "2020-04-07"
["rejectCode"]=>
NULL
["label"]=>
string(20) "PRLV SEPA ONLINE SAS"
["reference"]=>
string(7) "0000000"
["exemptCode"]=>
string(1) "0"
["amount"]=>
float(-22.79)
["bankOperationReference"]=>
NULL
}
Detail LIB: DEDIBOX 3706114
  • getCode() is the operation code of the CFONB, B1 here: operation codes gives its label.
  • getDate() is the operation date, getValueDate() the value date.
  • Fields left blank in the file are null for the nullable getters (getRejectCode(), getBankOperationReference() above); the others throw a ParseException when blank, except the reference in non-strict mode.
  • getDetails() lists the 05 lines of the operation, in order: their getQualifier() (LIB for an extra label line) tells what getAdditionalInformations() holds.

All the fields, with their positions in the line: CFONB 120 objects.

foreach ($reader->parse($content) as $statement) {
foreach ($statement->getOperations() as $operation) {
echo "Date: " . $operation->getDate()->format('Y-m-d') . "\n";
echo "Value Date: " . $operation->getValueDate()->format('Y-m-d') . "\n";
echo "Amount: " . $operation->getAmount() . "\n";
echo "Label: " . $operation->getLabel() . "\n";
echo "Reference: " . $operation->getReference() . "\n";
echo "Bank Code: " . $operation->getBankCode() . "\n";
echo "Account: " . $operation->getAccountNumber() . "\n";
// Optional fields are null when blank
if (null !== $operation->getInternalCode()) {
echo "Internal Code: " . $operation->getInternalCode() . "\n";
}
// Operation details (additional information)
foreach ($operation->getDetails() as $detail) {
echo "Additional Info: " . $detail->getAdditionalInformations() . "\n";
}
}
}

Balance and operation amounts are floats, signed: credits are positive, debits are negative. In the file, an amount is 13 digits and a sign character, which also holds the last digit; the number of decimals comes from position 20 of the line:

Last character Sign Last digit
{, A…I positive 0, 1…9
}, J…R negative 0, 1…9

0000000166956E with 2 decimals is 16695.65, with 0 decimals 1669565; 0000000000227R with 2 decimals is -22.79.

Dates are DateTimeImmutable objects at midnight, in the default time zone of PHP, read from DDMMYY: years 00 to 69 are 2000 to 2069, 70 to 99 are 1970 to 1999.

A date that does not exist throws a ParseException instead of rolling over to the next month: day 00 or 32, month 00 or 13, 31 April, 29 February of a non-leap year. 320420 fails with Unable to parse date "320420".

Every CFONB 120 date is mandatory, the value date of an operation included. Some banks write a date they do not have as zeros: 000000 fails with Unable to parse date "000000": the date is zero-filled (empty), but this field is mandatory, in strict and non-strict mode alike.

In strict mode, any line that cannot be parsed throws a ParseException. Non-strict mode only relaxes one rule: a blank operation reference on a 04 line becomes an empty string instead of an error. Every other error is still thrown, the order of the records and zero-filled dates included.

use Silarhi\Cfonb\Cfonb120Reader;
use Silarhi\Cfonb\Exceptions\ParseException;
$reader = new Cfonb120Reader();
// Strict mode (default) - throws an exception on parsing errors
try {
$statements = $reader->parse($content); // strict=true by default
} catch (ParseException $e) {
echo "Parsing error: " . $e->getMessage() . "\n";
}
// Non-strict mode - blank operation references are returned as ''
$statements = $reader->parse($content, false); // strict=false

With a file whose two operations have no reference:

blank-references.txt
0110278 02204EUR2 00020427603 070420 0000000166728F070420080420
0410556677202204EUR2 00020427603B1080420 080420PRLV SEPA FREE MOBILE 0000000000201J
0410278000202204EUR2 0002042760306080420 080420VIR JOHNDOE / FOOBAR 0000000050000}
0710278 02204EUR2 00020427603 080420 0000000116527E
cfonb120-strict.php
<?php
use Silarhi\Cfonb\Cfonb120Reader;
use Silarhi\Cfonb\Exceptions\ParseException;
$reader = new Cfonb120Reader();
$content = file_get_contents('blank-references.txt');
// Strict mode (the default): a blank operation reference is an error
try {
$reader->parse($content);
} catch (ParseException $e) {
echo sprintf("Strict: %s\n", $e->getMessage());
}
// Non-strict mode: a blank operation reference becomes an empty string
foreach ($reader->parse($content, false) as $statement) {
foreach ($statement->getOperations() as $operation) {
echo sprintf("Non-strict: %s, reference %s\n", $operation->getLabel(), var_export($operation->getReference(), true));
}
}
Output
Strict: The value of key "reference" is not a string
Non-strict: PRLV SEPA FREE MOBILE, reference ''
Non-strict: VIR JOHNDOE / FOOBAR, reference ''