andreaselia/laravel-firefly
A Laravel wrapper for the Firefly III API. Connect your app to a Firefly III instance to manage accounts, transactions, budgets, categories, and reports through a simple, fluent PHP interface. Configure your API token and start building finance integrations quickly.
This package is a thin, config-driven wrapper around the Firefly III REST API. You still need a running Firefly III instance and a personal access token; the package only handles the base URL, auth header, and Laravel wiring.
Minimal setup
composer require andreaselia/laravel-firefly
php artisan vendor:publish --provider="AndreasElia\Firefly\FireflyServiceProvider"
Set the connection values in .env (names may vary slightly—check the published config/firefly.php):
FIREFLY_URL=https://firefly.example.com
FIREFLY_TOKEN=your-personal-access-token
Generate the token in Firefly III under Options → Profile → OAuth → Personal Access Tokens. The token grants full API access, so treat it like a password.
Where to look first
config/firefly.php — confirms the exact env keys and whether the base URL includes /api/v1.src/FireflyServiceProvider.php and the facade class — confirms the container binding and facade alias.src/ — these map to Firefly endpoints (accounts, transactions, budgets, etc.) and define the available methods.First use case
Pull accounts into a dashboard or select list:
use AndreasElia\Firefly\Facades\Firefly;
$accounts = Firefly::accounts()->all();
foreach ($accounts['data'] as $account) {
// $account['id'], $account['attributes']['name'], ...
}
Push a transaction from a form or webhook:
Firefly::transactions()->create([
'type' => 'withdrawal',
'date' => now()->toIso8601String(),
'amount' => '12.50',
'description' => 'Coffee',
'source_id' => $assetAccountId,
'destination_id' => $expenseAccountId,
]);
The payloads mirror the Firefly III API, so the official API docs are the real reference once the wrapper is configured.
Wrap the facade in a domain service. Avoid calling Firefly:: directly from controllers. A small service makes the integration testable and gives you one place to normalize Firefly’s JSON:API responses.
class FireflyAccountService
{
public function all(): Collection
{
return collect(Firefly::accounts()->all()['data'])
->map(fn (array $account) => [
'id' => $account['id'],
'name' => $account['attributes']['name'],
'type' => $account['attributes']['type'],
]);
}
}
Separate read and write workflows. Most apps do one of two things:
Read/sync pattern: a scheduled command pulls accounts, transactions, or budgets and upserts them locally. Paginate explicitly and store the Firefly ID as a unique key.
$page = 1;
do {
$response = Firefly::transactions()->all(['page' => $page]);
foreach ($response['data'] as $transaction) {
LocalTransaction::updateOrCreate(
['firefly_id' => $transaction['id']],
['payload' => $transaction],
);
}
$page++;
} while ($page <= ($response['meta']['pagination']['total_pages'] ?? 1));
Write-through pattern: after a local event (order paid, invoice created, expense submitted), dispatch a queued job that creates the Firefly transaction. Store the returned Firefly ID on your model so retries are idempotent.
dispatch(new CreateFireflyTransaction($order));
Queue and throttle writes. Firefly III is usually self-hosted and not built for high write volume. Use queued jobs with ->onQueue('firefly') and a rate limiter or Bus::batch() for backfills.
Keep an account map. Firefly transactions reference source_id and destination_id, not account names. Cache a name => id map locally rather than hitting /accounts on every write.
Handle Firefly webhooks if you need near-real-time data. Firefly can POST transaction events to your app. Put a route behind signature verification, then refresh the affected local record. This is often cheaper than polling.
Test with Http::fake(). The wrapper ultimately uses Laravel’s HTTP client or Guzzle, so faking the Firefly endpoints keeps tests fast and deterministic:
Http::fake([
'firefly.example.com/api/v1/accounts*' => Http::response([
'data' => [/* ... */],
]),
]);
$this->artisan('firefly:sync-accounts')->assertSuccessful();
composer.json and smoke-test against your Firefly III version. Newer Firefly fields or endpoints may require raw HTTP calls or a fork.$account['attributes']['name'] ?? null) and centralize parsing in a service.Y-m-d depending on the endpoint. Use now()->toIso8601String() for timestamps and now()->toDateString() for date-only fields.transactions array of splits, each with its own amount, source, and destination. If the wrapper exposes create() for a single split, confirm the shape it sends before relying on it for transfers or multi-split transactions.FIREFLY_URL to the instance root (https://firefly.example.com). If the wrapper appends /api/v1, adding it yourself produces /api/v1/api/v1. Check the published config and test with php artisan tinker.verify => false (development only) or a CA bundle path.FIREFLY_URL or FIREFLY_TOKEN, run php artisan config:clear locally or php artisan config:cache in deployment. Stale config is a common “why is it hitting the old host?” cause.all() likely returns one page, not everything. Always inspect meta.pagination and loop or pass per_page explicitly.422 with a JSON message and errors object for validation failures. Catch RequestException, log the response body, and surface a useful message rather than a generic 500.How can I help you explore Laravel packages today?