Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Fortie Laravel Package

wetcat/fortie

Fortie is a Laravel package that offers a simple, customizable authentication UI and scaffolding powered by Laravel Fortify. It helps you quickly set up login, registration, password reset, email verification, and account management without building forms from scratch.

View on GitHub
Deep Wiki
Context7

Getting Started

Fortie is a thin, opinionated Laravel wrapper around the Fortnox REST API (v3). It handles OAuth2 with Fortnox, token storage/refresh, and a repository-per-endpoint abstraction (Fortie::customers(), Fortie::invoices(), Fortie::vouchers(), etc.). It is not a full ORM — think "fluent HTTP client with a Laravel-shaped facade."

Minimal setup:

  1. Install and publish config

    composer require wetcat/fortie
    php artisan vendor:publish --provider="Wetcat\Fortie\FortieServiceProvider"
    

    Publish both the config (config/fortie.php) and, if using database token storage, the migrations (typically a fortnox_tokens table).

  2. Register a Fortnox integration in the Fortnox developer portal. You need:

    • A client_id and client_secret
    • An exact redirect_uri (Fortnox is strict about this — no wildcards)
    • The list of scopes you intend to request (e.g. companyinformation, customer, invoice, order, article, supplier, bookkeeping, voucher)
  3. Set environment variables:

    FORTNOX_CLIENT_ID=
    FORTNOX_CLIENT_SECRET=
    FORTNOX_REDIRECT_URI=https://your-app.test/fortnox/callback
    FORTNOX_SCOPES=companyinformation,customer,invoice
    
  4. Wire up the OAuth callback. Two routes + a controller:

    Route::get('/fortnox/redirect', fn () => redirect(Fortie::authorizationURL()));
    Route::get('/fortnox/callback', FortnoxCallbackController::class);
    

    The callback exchanges the code for tokens and persists them via Fortie's configured storage driver.

First use case: fetch company information to prove the wiring works before touching business data.

$company = Fortie::companyInformation()->get();

Then hit Fortie::customers()->all() to verify scopes and pagination. If these two calls work, everything else is incremental.

The one place to read first is config/fortie.php — it tells you the storage driver, scopes, base URI, and the token refresh strategy in one glance. Read it before reading any source.

Implementation Patterns

OAuth connection as a first-class entity, not a session artifact. Fortnox tokens are per-company. If your app serves multiple tenants (typical for an accounting-adjacent SaaS), do not put tokens in the session or a global cache key. Store one token row per tenant/company, keyed by something your app understands. Wrap the connection in a model with connect(), disconnect(), isConnected(), and use that as your integration boundary.

Authorization URL generation in a controller, not a view. The state parameter matters — Fortnox will round-trip it, and you should verify it on callback to prevent CSRF on the OAuth handshake. Generate it, stash it in the session, compare on return.

Repository calls as the fluent layer, services on top. Fortie's repos are close to raw HTTP. Treat them the way you'd treat an Eloquent query builder: fine inside a service, wrong to see in a controller. Example service:

class FortnoxInvoiceSync
{
    public function push(Invoice $invoice): array
    {
        return Fortie::invoices()->create([
            'CustomerNumber' => $invoice->customer->fortnox_number,
            'InvoiceRows'    => $invoice->rows->map(fn ($r) => [
                'ArticleNumber' => $r->article_number,
                'DeliveredQuantity' => $r->quantity,
            ])->all(),
            'InvoiceDate' => $invoice->issued_at->toDateString(),
        ]);
    }
}

Note the PascalCase keys — Fortnox payloads are PascalCase, and Fortie passes them through. Do not camelCase them.

Queue anything that iterates. A nightly "sync all customers" job is the canonical pattern. Combine Fortie::customers()->all() (which yields paginated results) with a RateLimited job middleware or Bus::batch() sized well under Fortnox's limits. Do not run loops of API calls from a web request — Fortnox will rate-limit you, and 429s are not retried gracefully unless you code that.

Refresh proactively, not reactively. Tokens are short-lived (access ~1 hour, refresh ~45 days with rotation). Fortie's storage layer will refresh on expiry, but in a multi-tenant app you want a scheduled job that refreshes any token expiring in the next N minutes, so a web request never eats the latency — and so you catch the failure mode where a refresh token has silently expired (user must re-authorize) before a customer complains.

Feature-test with HTTP fakes. Because Fortie ultimately talks to api.fortnox.se, use Laravel's Http::fake() in tests and assert on outbound request shape. This is much cheaper than mocking Fortie internals, and it catches the PascalCase-key mistakes that are the #1 source of "why is my invoice rejecting" tickets.

Gotchas and Tips

Refresh tokens rotate. Fortnox returns a new refresh token on each refresh. If your storage layer only updates access_token and ignores the new refresh_token, the integration silently dies after the first refresh. Verify your storage driver persists both. If you wrote a custom driver, this is the bug you will have.

Scopes are fixed at consent time. Adding a scope to config/fortie.php does not retroactively grant it — existing connected tenants must re-authorize. Design for this: a disconnect() + reconnect flow, and a UI that says "Fortnox permissions need to be renewed." Mismatches surface as a 403 with ErrorInformation.Code = 2000315 or similar, which is not self-explanatory.

Rate limits are aggressive and per-client. Fortnox enforces something on the order of 25 requests per 5 seconds per client. A naive foreach over 500 customers will fail halfway. Two mitigations: throttle queue jobs (->middleware(new RateLimited('fortnox')) with a limiter defined as RateLimiter::for('fortnox', fn () => Limit::perSecond(4))), and prefer Fortnox's filter query params over client-side filtering so you make one request instead of many.

Pagination lives under MetaInformation, not meta. Responses wrap collections in a MetaInformation block with @CurrentPage, @TotalPages, @TotalResources. Iterate until @CurrentPage >= @TotalPages. Fortie's all() typically handles this, but search() or raw calls may not — check before trusting a count.

Number assignment is Fortnox's job. CustomerNumber, InvoiceNumber, OrderNumber, and voucher VoucherNumber are assigned server-side unless you're explicit. Don't invent IDs in your app expecting Fortnox to honor them; store the returned number on your model. For invoices, Finalpaydate, Total, Balance, and Booked are also server-controlled — read-only.

Error handling: Fortnox returns {"ErrorInformation": {"Error": "...", "Message": "...", "Code": 2000xxx}} with a non-2xx status. Fortie generally surfaces these as exceptions (GuzzleHttp\Exception\ClientException or a package-specific exception depending on version). Catch RequestException, read $e->getResponse()->json()['ErrorInformation'], and log Code — the numeric code is what Fortnox support actually wants.

Vouchers must balance. If you're using the bookkeeping scope, every voucher's VoucherRows must net to zero or Fortnox rejects it. This is the single most common integration failure for anyone writing journal entries. Validate before sending; unit-test the balance check.

Dates and decimals: Fortnox expects Y-m-d dates and decimals as numeric (not strings, not floats with locale). Laravel's ->toDateString() is safe; Carbon::toDateTimeString() is not. Use number_format($x, 2, '.', '') if your money values are stored as strings.

Sandbox vs. production: Fortnox requires a separate developer account/company for sandbox. The base URI differs (usually https://api.fortnox.se/3 for both, but the client credentials are distinct). Keep this in env, not in code.

Multi-tenancy: Fortie's default storage assumes one token. The moment you add a second tenant you need either a custom TokenStorage implementation bound in the service provider or manual token plumbing. This is the documented extension point — use it rather than reaching into package internals.

Extension points worth knowing: a custom token storage class (implements the package's storage contract) and binding the Guzzle client options via config if you need proxies/timeouts. Everything else is best done in your own service layer — subclassing Fortie's repositories to add domain methods (e.g. createFromInvoice()) is cleaner than sprinkling Fortie:: calls throughout your app.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle