Turn any invoice (PDF, scan, or photo) into clean, queryable database rows and a ready-made admin UI, with one command.
laravel-invoice-manager is the batteries-included layer on top of the SharpAPI Invoice Parser. The SharpAPI endpoint reads an invoice and returns 100+ structured fields. This package takes that result and does the part you would otherwise build by hand: a battle-tested database schema, Eloquent models, an idempotent importer, a queued job, an artisan command, and optional Laravel Nova resources.
You run one command. You get a fully populated invoices table with line items, tax details, seller and buyer parties, bank details, and the complete raw payload, all related and ready to query.
flowchart TB
FILE["Invoice file<br/>PDF / JPG / PNG / TIFF / DOC / DOCX"]
CLI["Artisan command<br/>invoice-manager:import"]
JOB["Queued job<br/>ParseAndStoreInvoiceJob"]
PS["InvoiceParserService<br/>(sharpapi/laravel-invoice-parser)"]
API[("SharpAPI<br/>Invoice Parser API")]
IMP["InvoiceImporter<br/>map + persist<br/>(DB transaction, idempotent)"]
DB[("Database<br/>invoice_* tables")]
EVT(["InvoiceImported event"])
NOVA["Laravel Nova admin<br/>(optional, auto-registered)"]
APP["Your code:<br/>storeMany(result)"]
FILE --> CLI
FILE --> JOB
CLI --> PS
JOB --> PS
PS -->|"parseInvoice()"| API
API -->|"fetchResults(): result JSON"| PS
CLI --> IMP
JOB --> IMP
APP --> IMP
IMP --> DB
IMP --> EVT
DB --> NOVA
classDef pkg fill:#eef6ff,stroke:#3b82f6,color:#0b3a82;
classDef ext fill:#fff7ed,stroke:#fb923c,color:#7c2d12;
class CLI,JOB,IMP,EVT,NOVA pkg;
class PS,API ext;
This package owns the blue boxes: the import command and job, the InvoiceImporter, the InvoiceImported event, and the optional Nova admin. The parsing itself is delegated to sharpapi/laravel-invoice-parser, which calls the SharpAPI endpoint. See How an import flows for the runtime sequence and Data model for the full schema.
Manual invoice processing costs businesses an average of $15 to $40 per invoice once you add up labor, errors, and delays. It is the single biggest source of accounts payable mistakes. The SharpAPI Invoice Parser takes that from minutes per invoice to seconds and removes manual data entry entirely.
But the parser hands you JSON. To actually use it you still need a schema, models, a mapper that handles missing fields and empty strings, idempotency so retries do not duplicate rows, and an admin screen so your team can see what came in. That is days to weeks of work, and it is the same work for every project.
This package is that work, done once, the right way:
- Dream outcome: every field from any invoice, in your own database, queryable with Eloquent, visible in a Nova admin, feeding your ERP or reconciliation pipeline.
- Proven and reliable: a normalized schema, an importer wrapped in a transaction, idempotent imports keyed on the SharpAPI job id, and a Pest test suite that runs with zero network calls.
- Near-zero time delay:
composer require,php artisan migrate, thenphp artisan invoice-manager:import invoice.pdf --sync. Rows appear. - Near-zero effort: no OCR to train, no parsing rules to maintain, no schema to design, no edge cases to chase. The AI does extraction, this package does persistence.
If you process invoices in Laravel, this is the shortest path from a file on disk to structured, managed data.
- One-command import:
invoice-manager:import {path}parses with SharpAPI and stores everything. - Complete schema: invoices, parties (seller and buyer), bank details, line items, and tax details, with frequently-queried values as real columns and rarely-queried groups as JSON.
- Idempotent imports: re-running the same SharpAPI job never creates duplicate rows (configurable: update, skip, or always new).
- Queued processing: dispatch
ParseAndStoreInvoiceJobto handle invoices in the background and at scale. - Optional Laravel Nova admin: ready-made resources that activate automatically when Nova is installed, and stay out of the way when it is not.
- Clean data guarantees: empty strings become
null, dates become realdatecolumns, money isdecimal(15,2). - Fully configurable and publishable: table prefix, model classes, queue, and Nova behaviour are all config-driven.
This package builds on top of sharpapi/laravel-invoice-parser, the thin SDK that calls the SharpAPI endpoint. It does not reimplement the API call; it depends on the parser and reuses its InvoiceParserService.
| Package | Responsibility |
|---|---|
sharpapi/laravel-invoice-parser |
Calls the SharpAPI endpoint, returns the raw structured JSON. |
sharpapi/laravel-invoice-manager |
Stores that JSON in a managed database and gives you models, jobs, a command, and a Nova admin. |
Install the manager and the parser comes with it.
- Accounts payable automation: vendor invoices land as structured rows, ready for approval and posting.
- ERP and accounting sync: feed parsed invoices into SAP, Oracle, NetSuite, QuickBooks, or Xero.
- Spend analytics and vendor management: query line items, totals, and tax across every supplier.
- Audit and compliance: keep the full raw payload alongside the normalized data for traceability.
- Document digitization: convert scanned and photographed invoices into clean database records.
- PHP >= 8.1
- Laravel >= 10.48.29
- A SharpAPI API key (free to start at SharpAPI.com)
- Laravel Nova (optional, only if you want the admin UI)
- Install the package via
composer:
composer require sharpapi/laravel-invoice-manager-
Register at SharpAPI.com to obtain your API key.
-
Set the API key in your
.envfile (used by the underlying parser):
SHARP_API_KEY=your_api_key_here- Run the migrations (tables are created with the
invoice_prefix by default):
php artisan migrate- [OPTIONAL] Publish the configuration file:
php artisan vendor:publish --tag=sharpapi-invoice-manager-config- [OPTIONAL] Publish the migrations to customize them before running:
php artisan vendor:publish --tag=sharpapi-invoice-manager-migrationsParse and store an invoice inline (hits SharpAPI with your key):
php artisan invoice-manager:import /path/to/invoice.pdf --syncOn success it prints a table of the stored invoices:
+----+-----------+----------+--------+
| ID | Invoice # | Currency | Total |
+----+-----------+----------+--------+
| 1 | INV-1688 | MYR | 373.00 |
+----+-----------+----------+--------+
Drop --sync to dispatch the work to a queue instead:
php artisan invoice-manager:import /path/to/invoice.pdfuse SharpAPI\InvoiceManager\Jobs\ParseAndStoreInvoiceJob;
ParseAndStoreInvoiceJob::dispatch('/path/to/invoice.pdf');The job parses the file with SharpAPI, then persists every invoice in the result. Configure the queue connection and name in config/sharpapi-invoice-manager.php.
If you already have a parse result (for example from a webhook), store it directly:
use SharpAPI\InvoiceManager\Services\InvoiceImporter;
use SharpAPI\InvoiceParser\InvoiceParserService;
$parser = app(InvoiceParserService::class);
$importer = app(InvoiceImporter::class);
$statusUrl = $parser->parseInvoice('/path/to/invoice.pdf');
$job = $parser->fetchResults($statusUrl);
$invoices = $importer->storeMany($job->getResultArray(), [
'sharp_api_job_id' => $job->id,
'source_file_path' => '/path/to/invoice.pdf',
'source_file_name' => 'invoice.pdf',
]);
$invoice = $invoices->first();
$invoice->invoice_number; // "INV-1688"
$invoice->total; // total_payable, falling back to total_incl_tax
$invoice->lineItems; // collection of line items
$invoice->seller->name; // "L Dairy Foods Sdn Bhd"
$invoice->buyer->billing_city; // "Kuala Lumpur"
$invoice->taxDetails; // collection of tax breakdownsEvery successful import fires an event:
use SharpAPI\InvoiceManager\Events\InvoiceImported;
// In a listener
public function handle(InvoiceImported $event): void
{
$invoice = $event->invoice;
// notify, post to ERP, reconcile, etc.
}sequenceDiagram
autonumber
participant App as Your app (command or job)
participant Parser as InvoiceParserService
participant API as SharpAPI
participant Importer as InvoiceImporter
participant DB as Database
participant Event as InvoiceImported
App->>Parser: parseInvoice(filePath)
Parser->>API: POST /finance/parse_invoice
API-->>Parser: status_url
App->>Parser: fetchResults(status_url)
Parser->>API: poll until job done
API-->>Parser: result JSON (array of invoices)
App->>Importer: storeMany(result, meta)
loop each invoice element
Importer->>DB: begin transaction
Importer->>DB: upsert invoice (idempotent on sharp_api_job_id)
Importer->>DB: insert parties, bank details, line items, tax details
Importer->>DB: commit
Importer-)Event: dispatch(InvoiceImported)
end
Importer-->>App: Collection of Invoice models
On import the mapper coerces empty strings to null, numeric strings to numbers, and date strings to Carbon dates. The whole element is also stored verbatim in raw_payload for lossless re-processing.
All tables use the configurable invoice_ prefix (shown without the prefix below). Money columns are decimal(15,2), dates are real date columns, and empty strings from the API are coerced to null.
| Table | Holds |
|---|---|
invoices |
One row per detected invoice: document, invoice, and financial scalars as real columns; references, e-invoice, sales info, payment, logistics, and source pages as JSON; plus the full raw_payload. |
invoice_parties |
One seller and one buyer per invoice, with billing and delivery addresses flattened to columns. |
invoice_bank_details |
Bank accounts belonging to a party. |
invoice_line_items |
Every line item, with money and quantities as decimals and date fields as real dates. |
invoice_tax_details |
Tax breakdown rows (type, rate, taxable amount, tax amount). |
erDiagram
invoices ||--o{ invoice_parties : "parties (seller, buyer)"
invoices ||--o{ invoice_line_items : lineItems
invoices ||--o{ invoice_tax_details : taxDetails
invoice_parties ||--o{ invoice_bank_details : bankDetails
invoices {
bigint id PK
uuid uuid "indexed"
string sharp_api_job_id UK "nullable; idempotency key"
string source_file_path "nullable"
string source_file_name "nullable"
string document_type
string original_type_label
boolean is_invoice
boolean is_copy
string copy_type
string invoice_number "indexed"
date issue_date "indexed"
date due_date
date document_date
date order_date
date delivery_date
date shipping_date
date pricing_date
string currency "3 chars"
decimal exchange_rate "15,6"
string page_info
text amount_in_words
text notes
text remarks
text delivery_instructions
decimal late_payment_interest_rate "8,4"
json terms_and_conditions
decimal subtotal "15,2"
decimal gross_amount "15,2"
decimal total_discount_amount "15,2"
decimal shipping_charge "15,2"
decimal delivery_fee "15,2"
decimal total_excl_tax "15,2"
decimal total_tax_amount "15,2"
decimal service_tax_amount "15,2"
decimal total_incl_tax "15,2"
decimal rounding_adjustment "15,2"
decimal total_payable "15,2"
decimal amount_paid "15,2"
decimal amount_due "15,2"
json references
json e_invoice
json sales_info
json payment
json logistics
json source_pages
json raw_payload
timestamp created_at
timestamp updated_at
}
invoice_parties {
bigint id PK
bigint invoice_id FK
string role "seller or buyer"
string name
string trade_name
string registration_number
string tin
string brn
string sst_id
string gst_id
string vat_id
string msic_code
string business_activity
string customer_account_number
string billing_location_name
string billing_recipient_name
string billing_street_line_1
string billing_street_line_2
string billing_city
string billing_state
string billing_postcode
string billing_country
string delivery_recipient_name
string delivery_location_name
string delivery_street_line_1
string delivery_street_line_2
string delivery_city
string delivery_state
string delivery_postcode
string delivery_country
boolean delivery_address_same_as_billing
string phone
string fax
string email
string website
json contact_person
json attention_to
timestamp created_at
timestamp updated_at
}
invoice_bank_details {
bigint id PK
bigint invoice_party_id FK
string bank_name
string account_name
string account_number
string sort_code
string swift_code
string iban
timestamp created_at
timestamp updated_at
}
invoice_line_items {
bigint id PK
bigint invoice_id FK
integer line_number "indexed"
string item_code
string stock_code
string barcode
text description
string classification_code
string country_of_origin
decimal quantity "15,4"
decimal free_quantity "15,4"
string unit_of_measure
string unit_of_measure_raw
string pack_size
decimal total_units "15,4"
decimal weight "15,4"
string weight_uom
decimal unit_price "15,4"
decimal discount_percent "8,4"
decimal discount_amount "15,2"
decimal subtotal "15,2"
decimal tax_rate "8,4"
string tax_type
decimal tax_amount "15,2"
decimal total_excl_tax "15,2"
decimal total_incl_tax "15,2"
date expiry_date
string batch_lot_number
date service_start_date
date service_end_date
timestamp created_at
timestamp updated_at
}
invoice_tax_details {
bigint id PK
bigint invoice_id FK
string tax_type
decimal tax_rate "8,4"
decimal taxable_amount "15,2"
decimal tax_amount "15,2"
timestamp created_at
timestamp updated_at
}
The package ships five Eloquent models (namespace SharpAPI\InvoiceManager\Models). Each can be swapped for your own subclass via the models.* config keys.
classDiagram
class Invoice {
+lineItems() HasMany
+taxDetails() HasMany
+parties() HasMany
+seller() HasOne
+buyer() HasOne
+getTotalAttribute() string
}
class InvoiceParty {
+invoice() BelongsTo
+bankDetails() HasMany
+scopeSellers() Builder
+scopeBuyers() Builder
}
class InvoiceBankDetail {
+party() BelongsTo
}
class InvoiceLineItem {
+invoice() BelongsTo
}
class InvoiceTaxDetail {
+invoice() BelongsTo
}
Invoice "1" --> "*" InvoiceLineItem : lineItems
Invoice "1" --> "*" InvoiceTaxDetail : taxDetails
Invoice "1" --> "*" InvoiceParty : parties
Invoice "1" --> "1" InvoiceParty : seller / buyer
InvoiceParty "1" --> "*" InvoiceBankDetail : bankDetails
| Model | Backing table | Config key | Relationships and helpers |
|---|---|---|---|
Invoice |
invoices |
models.invoice |
lineItems, taxDetails, parties, seller, buyer, total accessor |
InvoiceParty |
invoice_parties |
models.party |
invoice, bankDetails, sellers() / buyers() scopes |
InvoiceBankDetail |
invoice_bank_details |
models.bank_detail |
party |
InvoiceLineItem |
invoice_line_items |
models.line_item |
invoice |
InvoiceTaxDetail |
invoice_tax_details |
models.tax_detail |
invoice |
config/sharpapi-invoice-manager.php:
return [
'table_prefix' => env('INVOICE_MANAGER_TABLE_PREFIX', 'invoice_'),
'import' => [
// update | skip | new
'on_duplicate' => env('INVOICE_MANAGER_ON_DUPLICATE', 'update'),
],
'queue' => [
'connection' => env('INVOICE_MANAGER_QUEUE_CONNECTION'),
'name' => env('INVOICE_MANAGER_QUEUE'),
],
'nova' => [
'enabled' => env('INVOICE_MANAGER_NOVA', true),
'resource' => \SharpAPI\InvoiceManager\Nova\Invoice::class,
],
'models' => [
'invoice' => \SharpAPI\InvoiceManager\Models\Invoice::class,
'party' => \SharpAPI\InvoiceManager\Models\InvoiceParty::class,
'line_item' => \SharpAPI\InvoiceManager\Models\InvoiceLineItem::class,
'tax_detail' => \SharpAPI\InvoiceManager\Models\InvoiceTaxDetail::class,
'bank_detail' => \SharpAPI\InvoiceManager\Models\InvoiceBankDetail::class,
],
];Point any models.* entry at your own subclass to add behaviour without forking the package.
If Laravel Nova is installed and nova.enabled is true, the package registers an Invoice resource automatically, with the line items, tax details, and parties exposed as related resources, and the raw JSON groups shown as read-only code fields. No manual registration required.
If Nova is not installed, nothing happens and the rest of the package works exactly the same.
The package ships five Nova resources (namespace SharpAPI\InvoiceManager\Nova), listed separately from the Eloquent models above. Only the top-level Invoice resource appears in the navigation; the rest are exposed as related (HasMany) sub-resources from the invoice detail view.
flowchart TB
INV["Invoice resource<br/>navigation group: Invoices<br/>index: invoice #, seller, buyer, currency, total, issue date<br/>panels: Document, Invoice, Financials, Raw Groups"]
PARTY["InvoiceParty resource<br/>hidden from navigation"]
LI["InvoiceLineItem resource<br/>hidden from navigation"]
TAX["InvoiceTaxDetail resource<br/>hidden from navigation"]
BANK["InvoiceBankDetail resource<br/>hidden from navigation"]
INV -->|"HasMany lineItems"| LI
INV -->|"HasMany taxDetails"| TAX
INV -->|"HasMany parties"| PARTY
PARTY -->|"HasMany bankDetails"| BANK
| Nova resource | Backing model | In navigation | Key fields and panels |
|---|---|---|---|
Nova\Invoice |
Invoice |
Yes (group "Invoices") | Index: invoice number, seller, buyer, currency, total, issue date. Detail panels: Document, Invoice, Financials, Raw Groups (references, e-invoice, payment, logistics, sales info, raw payload as read-only code fields). HasMany: line items, tax details, parties. |
Nova\InvoiceParty |
InvoiceParty |
No (sub-resource) | Role, name, VAT id, registration number, addresses, contact JSON. HasMany bank details. |
Nova\InvoiceLineItem |
InvoiceLineItem |
No (sub-resource) | Line number, description, quantity, unit price, subtotal, tax, totals, codes, expiry date. |
Nova\InvoiceTaxDetail |
InvoiceTaxDetail |
No (sub-resource) | Tax type, rate, taxable amount, tax amount. |
Nova\InvoiceBankDetail |
InvoiceBankDetail |
No (sub-resource) | Bank name, account name, account number, SWIFT, IBAN. |
To point the admin at your own customized resource, set nova.resource in the config to your subclass. The sub-resources are registered automatically alongside it.
composer testThe suite runs fully offline against a committed fixture using Orchestra Testbench and in-memory SQLite. No API key and no network access are required.
laravel-invoice-manager, invoice-management, laravel-invoice, ai-invoice-parser, accounts-payable, laravel-nova, nova-resource, invoice-database, invoice-ocr, sharpapi
For issues or suggestions, please:
Please see CHANGELOG for a detailed list of changes.
- A2Z WEB LTD
- Dawid Makowski
- Enhance your Laravel AI capabilities!
The MIT License (MIT). Please see License File for more information.
Stay updated with news, tutorials, and case studies:
