Laravel package for integration with the FocusNFe API.
Manage NFSe (electronic service invoices), companies, webhooks, and search municipalities — all through a clean, type-safe API with fully validated DTOs.
- PHP 8.2+
- Laravel 12 or 13
- Extensions:
json,curl
The package is not published on Packagist; install it straight from GitHub:
composer config repositories.larafocus vcs https://github.com/OwnerPro-Software/larafocus
composer require ownerpro/larafocusThe service provider is auto-discovered.
Publish the config file:
php artisan vendor:publish --provider="Larafocus\LarafocusServiceProvider"Add these to your .env:
LARAFOCUS_ENVIRONMENT=sandbox
LARAFOCUS_SANDBOX_TOKEN=your-sandbox-token
LARAFOCUS_PRODUCTION_TOKEN=your-production-token
LARAFOCUS_MASTER_TOKEN=your-master-token| Variable | Description | Default |
|---|---|---|
LARAFOCUS_ENVIRONMENT |
sandbox or production |
sandbox |
LARAFOCUS_SANDBOX_TOKEN |
Token for sandbox API | — |
LARAFOCUS_PRODUCTION_TOKEN |
Token for production API | — |
LARAFOCUS_MASTER_TOKEN |
Master token for Companies API | — |
LARAFOCUS_PREFIX |
API path prefix | /v2 |
LARAFOCUS_SANDBOX_ENDPOINT |
Sandbox base URL | https://homologacao.focusnfe.com.br |
LARAFOCUS_PRODUCTION_ENDPOINT |
Production base URL | https://api.focusnfe.com.br |
LARAFOCUS_TIMEOUT |
Total request timeout in seconds (emission can be slow) | 60 |
LARAFOCUS_CONNECT_TIMEOUT |
Connection timeout in seconds (a dead host fails fast) | 10 |
All interactions go through the Focus facade, which provides four resources:
use Larafocus\Focus;
Focus::nfse(); // NFSe operations
Focus::companies(); // Company management (requires master token)
Focus::hooks(); // Webhook management
Focus::search(); // Municipality, service, and tax code searchEvery method returns a FocusResponse with:
$response->statusCode; // int — HTTP status code
$response->success; // bool — true if status < 400
$response->data; // array — parsed JSON response
$response->errors; // array — extracted errors (if any)
$response->response; // Illuminate\Http\Client\Response — raw response
$response->isNotFound(); // bool — true when Focus reported the ref as non-existent (404)use Larafocus\Focus;
use Larafocus\Nfse\DTO\NfseRequest;
use Larafocus\Nfse\DTO\NfseCancelRequest;
use Larafocus\Nfse\DTO\NfseEmailRequest;
use Larafocus\Nfse\DTO\Prestador;
use Larafocus\Nfse\DTO\Tomador;
use Larafocus\Nfse\DTO\Servico;
use Larafocus\Nfse\DTO\Enums\NaturezaOperacao;
// Create
$response = Focus::nfse()->create('ref-001', new NfseRequest(
data_emissao: '2024-01-15T10:30:00-03:00',
natureza_operacao: NaturezaOperacao::TributacaoMunicipio,
optante_simples_nacional: true,
prestador: new Prestador(
cnpj: '12345678000195',
inscricao_municipal: '12345',
),
tomador: new Tomador(
cnpj: '98765432000187',
email: 'client@example.com',
),
servico: new Servico(
valor_servicos: 1500.00,
iss_retido: false,
item_lista_servico: '1.07',
discriminacao: 'Desenvolvimento de software',
codigo_municipio: '3550308',
),
));
// Get
$response = Focus::nfse()->get('ref-001');
// Cancel
$response = Focus::nfse()->cancel('ref-001', new NfseCancelRequest(
justificativa: 'Cancelamento solicitado pelo tomador',
));
// Send by email
$response = Focus::nfse()->email('ref-001', new NfseEmailRequest(
emails: ['recipient@example.com'],
));
// Trigger webhook
$response = Focus::nfse()->hook('ref-001');Each email must be a valid email address (validated with FILTER_VALIDATE_EMAIL). The list accepts 1 to 10 addresses.
Arrays are also accepted — they are converted to DTOs automatically:
$response = Focus::nfse()->create('ref-002', [
'data_emissao' => '2024-01-15T10:30:00-03:00',
'natureza_operacao' => '1',
'optante_simples_nacional' => true,
'prestador' => ['cnpj' => '12345678000195', 'inscricao_municipal' => '12345'],
'tomador' => ['cnpj' => '98765432000187'],
'servico' => [
'valor_servicos' => 1500.00,
'iss_retido' => false,
'item_lista_servico' => '1.07',
'discriminacao' => 'Desenvolvimento de software',
'codigo_municipio' => '3550308',
],
]);Requires LARAFOCUS_MASTER_TOKEN. Always hits the production endpoint.
use Larafocus\Focus;
use Larafocus\Companies\DTO\EmpresaRequest;
use Larafocus\Companies\DTO\Enums\RegimeTributario;
// List (with optional offset for pagination)
$response = Focus::companies()->list();
$response = Focus::companies()->list(10);
// Get
$response = Focus::companies()->get('company-id');
// Create
$response = Focus::companies()->create(new EmpresaRequest(
nome: 'Empresa LTDA',
nome_fantasia: 'Empresa',
cnpj: '12345678000195',
regime_tributario: RegimeTributario::SimplesNacional,
logradouro: 'Rua X',
numero: '123',
bairro: 'Centro',
municipio: 'Sao Paulo',
cep: '01310100',
uf: 'SP',
telefone: '1133334444',
email: 'contact@empresa.com',
habilita_nfse: true,
));
// Update
$response = Focus::companies()->update('company-id', new EmpresaRequest(
nome: 'Novo Nome LTDA',
));
// Delete
$response = Focus::companies()->delete('company-id');In sandbox environment, create() and update() automatically append ?dry_run=1.
use Larafocus\Focus;
use Larafocus\Hooks\DTO\WebhookRequest;
use Larafocus\Hooks\DTO\Enums\WebhookEvent;
// List all
$response = Focus::hooks()->list();
// Create
$response = Focus::hooks()->create(new WebhookRequest(
event: WebhookEvent::Nfse,
url: 'https://example.com/webhook/nfse',
));
// Get
$response = Focus::hooks()->get('hook-id');
// Delete
$response = Focus::hooks()->delete('hook-id');The webhook url must be a valid URL using https or http scheme.
use Larafocus\Focus;
// Municipalities
Focus::search()->cities()->list();
Focus::search()->cities()->get('3550308');
// Services for a city
Focus::search()->cities()->servicesFor('3550308')->list();
Focus::search()->cities()->servicesFor('3550308')->get('1.07');
// Tax codes for a city
Focus::search()->cities()->taxCodesFor('3550308')->list();
Focus::search()->cities()->taxCodesFor('3550308')->get('code-123');The FocusNFe API returns relative paths for file downloads (XML, cancellation XML). Use resolveFileUrl to build the full URL:
use Larafocus\Focus;
// Build download URL from a relative path
$xmlUrl = Focus::resolveFileUrl($data['caminho_xml_nota_fiscal']);
// => "https://homologacao.focusnfe.com.br/v2/nfse/abc123.xml"
$cancelXmlUrl = Focus::resolveFileUrl($data['caminho_xml_cancelamento']);
// => "https://homologacao.focusnfe.com.br/v2/nfse/abc123-cancelamento.xml"The relativePath must start with / and must not contain path traversal sequences (..) or null bytes.
The URL respects the current environment (sandbox or production).
use Larafocus\Focus;
use Larafocus\Infrastructure\Environment;
// Temporary override (returns a new instance, does not affect the singleton)
Focus::using(
environment: Environment::Production,
token: 'custom-token',
timeout: 30,
connectTimeout: 5,
)->nfse()->get('ref-001');
// Persistent change (mutates the singleton)
Focus::config(
environment: Environment::Production,
token: 'production-token',
);DTOs validate their input on construction. Invalid data throws InvalidDtoException:
use Larafocus\Shared\InvalidDtoException;
try {
Focus::nfse()->cancel('ref', new NfseCancelRequest(
justificativa: 'Too short', // must be 15-255 characters
));
} catch (InvalidDtoException $e) {
echo $e->getMessage();
}API errors are captured in FocusResponse::$errors without throwing:
$response = Focus::nfse()->get('nonexistent-ref');
if (! $response->success) {
foreach ($response->errors as $error) {
// Handle API error
}
}A response that arrives — including a definitive rejection such as erro_autorizacao,
404, 408, or 429 — is always returned as a FocusResponse. Only when no trustworthy
answer is obtained does the client throw a typed transport exception, all extending
Larafocus\Exceptions\CommunicationException:
| Exception | Meaning | Safe action |
|---|---|---|
RequestNotDeliveredException |
The request never reached Focus (DNS, connect, or TLS failure). | Retry directly — nothing was emitted or cancelled. |
IndeterminateResultException |
The request may have been processed (timeout, unreadable 2xx, or a 5xx on a write). |
Reconcile before retrying — the outcome is unknown. |
Because the ref deduplicates on Focus, reconciling with get($ref) never risks a duplicate:
use Larafocus\Exceptions\IndeterminateResultException;
use Larafocus\Exceptions\RequestNotDeliveredException;
try {
$response = Focus::nfse()->create($reference, $payload);
// handle success / erro_autorizacao / processando_autorizacao
} catch (RequestNotDeliveredException $e) {
// Nothing reached Focus — retrying the same call is safe.
} catch (IndeterminateResultException $e) {
// It may have been emitted — reconcile by reference.
$lookup = Focus::nfse()->get($reference);
if ($lookup->isNotFound()) {
// Not emitted — re-emitting the same ref is safe.
} else {
// Emitted — a webhook/verify will complete it.
}
}The IndeterminateResultException::$phase property (read, transfer, body, …) records
where the failure happened, for logging.
./vendor/bin/pest --coverage --min=100 --parallel
./vendor/bin/pest --mutate --min=100 --parallel
./vendor/bin/pest --type-coverage --min=100
./vendor/bin/phpstan analyse
./vendor/bin/psalm --taint-analysis
./vendor/bin/rector --dry-run
./vendor/bin/pint --testMIT. See LICENSE for details.