husail / cnab-sdk
SDK for reading and writing CNAB 240, 150 and 400 files, built on top of husail/edi-sdk.
Requires
- php: ^8.2
- husail/edi-sdk: ^1.0
- symfony/yaml: ^6.0|^7.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- pestphp/pest: ^4.7
- phpstan/phpstan: ^2.1
README
PHP SDK for reading and writing CNAB 240 files, built on top of husail/edi-sdk.
Provides tools to generate, parse and validate CNAB 240 files with support for payment grouping, automatic record sequencing, and bank-specific layouts.
๐ Requirements
- PHP 8.2+
husail/edi-sdk ^1.0symfony/yaml
๐ฆ Installation
composer require husail/cnab-sdk
๐ฆ Supported banks and formats
| Bank | CNAB 240 | CNAB 150 | CNAB 400 |
|---|---|---|---|
| Itaรบ (341) | โ Pagamentos | โ | โ |
| Bradesco (237) | ๐ | โ | โ |
๐ Writing a payment file โ Itaรบ CNAB 240
High-level API
use Husail\CnabSdk\Types\CnabDate; use Husail\CnabSdk\Banks\Itau\Cnab240Itau; use Husail\CnabSdk\Builder\CnabBatchBuilder; $file = Cnab240Itau::pagamentos() ->fileHeader([ 'company_type' => '2', // 2 = CNPJ 'company_document' => '12345678000195', 'bank_branch' => '00341', 'bank_account' => '123456789012', 'company_name' => 'ACME LTDA', 'generated_date' => CnabDate::toDate(now()), // DDMMAAAA 'generated_time' => CnabDate::toTime(now()), // HHMMSS 'sequence_number' => '000000001', ]) ->batch(function (CnabBatchBuilder $batch) use ($payments) { $batch->header([ 'operation_type' => 'C', // C = Credit 'payment_type' => '20', // Credit to account 'payment_form' => '01', 'company_type' => '2', 'company_document' => '12345678000195', 'bank_branch' => '00341', 'bank_account' => '123456789012', 'company_name' => 'ACME LTDA', ]); foreach ($payments as $payment) { $batch->add('segment_a', [ 'clearing_code' => '009', 'recipient_bank' => $payment->bankCode, 'bank_branch' => $payment->bankBranch, 'bank_account' => $payment->bankAccountNumber, 'recipient_name' => $payment->recipientName, 'payment_date' => CnabDate::toDate($payment->paymentDate), 'bank_ispb' => $payment->bankIspb, 'transfer_type' => '01', // TED 'amount' => $payment->amount, // float: 150.75 'recipient_document' => $payment->recipientDocument, ]); } }) ->toString();
batch_code, record_sequence, record_count (batch trailer) and batch_count, record_count (file trailer) are all filled automatically.
PIX by key โ Segment A + Segment B PIX
PIX by key requires a segment_b_pix after each segment_a.
The segment_b_pix is distinguished from the standard segment_b by the
pix_key_type field at positions 15-16.
->batch(function (CnabBatchBuilder $batch) use ($payments) { $batch->header(['operation_type' => 'C', 'payment_type' => '45', ...]); foreach ($payments as $payment) { $batch->add('segment_a', [ 'transfer_type' => 'PIX', 'amount' => $payment->amount, 'recipient_document' => $payment->recipientDocument, // other fields... ]); $batch->add('segment_b_pix', [ 'pix_key_type' => '04', // 04 = random key (EVP) 'recipient_type' => '2', // 2 = CNPJ 'recipient_document' => $payment->recipientDocument, 'pix_key' => $payment->pixKey, 'txid' => $payment->txid, // optional ]); } })
PIX key types (pix_key_type):
| Code | Type |
|---|---|
01 |
Phone |
02 |
|
03 |
CPF / CNPJ |
04 |
Random key (EVP) |
PIX by bank account โ Segment A + Segment B
PIX by bank account data uses the standard segment_b (address/email complement).
The parser distinguishes them automatically: positions 15-16 are spaces for
standard segment_b and a key type code for segment_b_pix.
$batch->add('segment_b', [ 'recipient_type' => '2', 'recipient_document' => $payment->recipientDocument, 'email' => $payment->email, // optional ]);
Multiple batches
Each ->batch() call creates a new batch with an incremented batch code:
Cnab240Itau::pagamentos() ->fileHeader([...]) ->batch(function (CnabBatchBuilder $batch) use ($tedPayments) { $batch->header(['operation_type' => 'C', 'payment_type' => '20', 'payment_form' => '01', ...]); foreach ($tedPayments as $p) { $batch->add('segment_a', [...]); } }) ->batch(function (CnabBatchBuilder $batch) use ($pixPayments) { $batch->header(['operation_type' => 'C', 'payment_type' => '45', 'payment_form' => '01', ...]); foreach ($pixPayments as $p) { $batch->add('segment_a', [...]); $batch->add('segment_b', [...]); } }) ->toString();
According to SISPAG rules, PIX payments must be in a separate file from other payment types.
Overriding auto-calculated fields
All automatically calculated fields can be overridden:
// Override batch trailer fields $batch->trailer([ 'total_amount' => 47075, ]); // Override file trailer fields ->fileTrailer([ 'batch_count' => 2, 'record_count' => 10, ])
๐ Reading a return file
The same layout covers both remessa (outgoing) and retorno (return).
The file_header.file_code field distinguishes them: 1 = remessa, 2 = retorno.
use Husail\CnabSdk\Banks\Itau\Cnab240Itau; use Husail\CnabSdk\Cnab; $content = file_get_contents('/path/to/retorno.txt'); $layout = Cnab240Itau::layout(); // Always validate before parsing $validation = Cnab::validate($content, $layout); if ($validation->fails()) { foreach ($validation->errors() as $error) { echo "Line {$error->line} [{$error->record}] {$error->field}: {$error->message}"; } } $result = Cnab::parse($content, $layout); // File header $header = $result->first('file_header'); echo $header?->get('company_name'); echo $header?->get('file_code'); // '1' = remessa, '2' = retorno echo $header?->get('generated_date'); // Segment A collection โ TED, DOC, PIX by bank account $segmentsA = $result->records('segment_a'); $segmentsA->count(); $segmentsA->each(function ($seg) { echo $seg->get('recipient_name'); echo $seg->get('amount'); // float echo $seg->get('occurrence_code'); // '00' = paid echo $seg->get('effective_date'); // filled by bank on return }); // Filter paid segments $paid = $segmentsA->filter(fn ($seg) => $seg->get('occurrence_code') === '00'); // Segment B PIX โ PIX by key complement $result->records('segment_b_pix')->each(function ($seg) { echo $seg->get('pix_key_type'); echo $seg->get('pix_key'); echo $seg->get('occurrence_code'); }); // Segment J โ boletos and concessionรกrias $result->records('segment_j')->each(function ($seg) { echo $seg->get('recipient_name'); echo $seg->get('payment_amount'); // float echo $seg->get('occurrence_code'); }); // Segment J52 โ payer/beneficiary data and QR Code $result->records('segment_j52')->each(function ($seg) { echo $seg->get('payer_document'); echo $seg->get('beneficiary_name'); echo $seg->get('pix_url'); });
Return occurrence codes (occurrence_code at positions 231-240):
| Code | Meaning |
|---|---|
00 |
Paid / Processed |
BD |
Invalid bank |
AB |
Scheduling error |
| Others | See Itaรบ SISPAG manual (v086) for the full list |
๐๏ธ Grouped payments
groupPayments() reconstructs each payment with all its segments together,
grouped by batch_code + record_sequence.
use Husail\CnabSdk\Banks\Itau\Cnab240Itau; use Husail\CnabSdk\Cnab; $result = Cnab::parse($content, Cnab240Itau::layout()); $payments = Cnab240Itau::groupPayments($result); $payments->count(); // total payments $payments->pixCount(); // PIX payments only $payments->boletoCount(); // boleto payments only
Generic access
foreach ($payments->payments() as $group) { $group->batchCode(); // '0001' $group->sequence(); // '00001' $group->get('segment_a')?->get('amount'); $group->get('segment_j')?->get('barcode'); $group->has('segment_b_pix'); // bool }
Typed PIX payments
foreach ($payments->pixPayments() as $pix) { $pix->batchCode(); $pix->sequence(); $pix->isPixByKey(); // true when segment_b_pix is present // segment_a โ always present $pix->segmentA()->get('amount'); // float $pix->segmentA()->get('recipient_name'); $pix->segmentA()->get('payment_date'); // segment_b โ PIX by bank account complement (optional) $pix->segmentB()?->get('email'); // segment_b_pix โ PIX by key complement (optional) $pix->segmentBPix()?->get('pix_key'); $pix->segmentBPix()?->get('pix_key_type'); // '04' = random key }
Typed boleto payments
foreach ($payments->boletoPayments() as $boleto) { $boleto->batchCode(); $boleto->sequence(); $boleto->hasPixQrCode(); // true when segment_j52 has a pix_url // segment_j โ always present $boleto->segmentJ()->get('barcode'); $boleto->segmentJ()->get('payment_amount'); // float $boleto->segmentJ()->get('due_date'); // segment_j52 โ payer/beneficiary and QR Code (optional) $boleto->segmentJ52()?->get('payer_name'); $boleto->segmentJ52()?->get('beneficiary_name'); $boleto->segmentJ52()?->get('pix_url'); }
Filter grouped payments
// Payments above R$ 1,000 $highValue = $payments->filter( fn ($group) => ($group->get('segment_a')?->get('amount') ?? 0) > 1000 ); // All PIX by key $pixByKey = $payments->filter( fn ($group) => $group->has('segment_b_pix') );
โ Validating a file
use Husail\CnabSdk\Banks\Itau\Cnab240Itau; use Husail\CnabSdk\Cnab; $result = Cnab::validate($content, Cnab240Itau::layout()); if ($result->passes()) { // safe to process } foreach ($result->errors() as $error) { echo "Line {$error->line} [{$error->record}] {$error->field}: {$error->message}"; }
๐ ๏ธ Generic API โ bring your own layout
Use Cnab::write() when you have a custom YAML or JSON layout:
use Husail\CnabSdk\Cnab; $layout = Cnab::fromYaml('/path/to/my-bank/cnab240-pagamentos.yaml'); $file = Cnab::write($layout) ->fileHeader([...]) ->batch(function ($batch) use ($payments) { $batch->header([...]); foreach ($payments as $p) { $batch->add('segment_a', [...]); } }) ->toString();
๐๏ธ CNAB helpers
use Husail\CnabSdk\Types\CnabDate; use Husail\CnabSdk\Types\Monetary; // Dates CnabDate::toDate(now()); // '08052026' CnabDate::toTime(now()); // '143052' CnabDate::fromDate('08052026'); // DateTimeImmutable CnabDate::zero(); // '00000000' CnabDate::isZero('00000000'); // true // Monetary values Monetary::serialize(150.75, decimalPlaces: 2, length: 15); // '000000000015075' Monetary::deserialize('000000000015075', decimalPlaces: 2); // 150.75 Monetary::format(150.75); // 'R$ 150,75'
๐๏ธ Adding a new bank
Implement BankLayoutInterface and provide a YAML layout file:
use Husail\CnabSdk\Contracts\BankLayoutInterface; use Husail\CnabSdk\Builder\CnabFileBuilder; use Husail\EdiSdk\Drivers\YamlDriver; use Husail\EdiSdk\Schema\FileLayout; class Cnab240MyBank implements BankLayoutInterface { public const BANK_CODE = '999'; public const BANK_NAME = 'MY BANK SA'; private const LAYOUT = __DIR__ . '/../../Layouts/MyBank/cnab240/pagamentos/layout.yaml'; public static function layout(): FileLayout { return (new YamlDriver())->load(self::LAYOUT); } public static function pagamentos(): CnabFileBuilder { return new CnabFileBuilder(self::layout()); } }
The layout YAML follows the same structure as the Itaรบ layout.
See src/Layouts/Itau/cnab240/pagamentos/layout.yaml as reference.
A single layout file covers both remessa and retorno.
๐ How it relates to edi-sdk
cnab-sdk
โโโ Provides: CNAB layouts (YAML), auto-counting builder, bank classes, payment grouping
โโโ Delegates to: husail/edi-sdk for write, parse and validate operations
edi-sdk
โโโ Provides: fixed-width file engine, layout drivers, sequence tree
Cnab::parse() and Cnab::validate() are thin wrappers over Edi::parse() and Edi::validate().
For full control, use the edi-sdk directly with Cnab240Itau::layout().
๐งช Testing
composer install
composer test
๐ค Contributing
Contributions, issues and pull requests are welcome.
If you find a bug or have a suggestion, feel free to open an issue.
๐ License
Licensed under the MIT License.