← Documentation home Developer

API Documentation v1

Base URL /api/v1 — Bearer token, company-scoped, JSON only. Create a token at Portal → Settings → API applications.

1. Overview

The API exposes the same ledger-backed engine as the portal. Every request is scoped to the company that owns the Bearer token. Creating, syncing or voiding through the API writes the same transactions and audit logs as the UI.

  • Auth: Authorization: Bearer gb_<60-char>
  • Content-Type: application/json — Accept: application/json
  • Pagination: ?page=1&per_page=15 (max 100)
  • Health: GET /up

2. Authentication — API applications

Portal → Settings → API applications (owner only). Click Create application, enter a name and optional expiry, and copy the token once. The token is stored hashed (SHA-256) and shown only once.

curl -H "Authorization: Bearer gb_abc..." https://app.example.com/api/v1/me
# 200 { "company_id": 1, "application": "Zapier" }
# 401 { "message": "Unauthenticated. Invalid API token." }
  • Tokens are company-scoped — they cannot read or write another company even if the ID is guessed.
  • Expired tokens return 401. Revoked (deleted) tokens return 401.
  • last_used_at is updated on every authenticated request.

3. Conventions

TopicRule
AmountsIntegers in minor units (cents). Invoices item unit_price accepts decimal string ("50.00") — converted via Money to cents.
DatesISO-8601 YYYY-MM-DD
PaginationLaravel paginator: data, current_page, last_page, total
Validation422 with errors object
Tenant isolationGlobal scope on company_id via CompanyContext set by AuthenticateApiApplication

4. Endpoints summary

MethodPathDescription
GET/api/v1/meToken info (company, app name)
GET/POST/api/v1/customers, /api/v1/customers/{id}List/create/show/update/delete customers
GET/POST/api/v1/products, /api/v1/products/{id}Products & services
GET/POST/api/v1/quotesQuotes/estimates
GET/POST/api/v1/invoices, /api/v1/invoices/{id}Invoices (draft-editable only)
GET/POST/api/v1/payments, /api/v1/payments/{id}/voidPayments + void
GET/POST/api/v1/expenses, /api/v1/expenses/{id}/voidExpenses + void
GET/POST/api/v1/vendorsVendors
GET/POST/api/v1/transactions, /transfer, /income, /adjustmentLedger

5. Customers

GET /api/v1/customers?search=acme&per_page=15
POST /api/v1/customers { "name":"Acme Corp","email":"acme@example.com" }
GET /api/v1/customers/{id}
PUT /api/v1/customers/{id}
DELETE /api/v1/customers/{id}  # soft delete

Fields: name*, email, phone, company_name, tax_id, billing_address, shipping_address

6. Products & Services

POST /api/v1/products { "name":"Widget","type":"product","unit_price":1000,"tax_rate":5 }

Types: product | service. unit_price is minor units.

7. Quotes

POST /api/v1/quotes {
  "customer_id":1, "issue_date":"2026-09-25", "expiry_date":"2026-10-25",
  "items":[{"description":"Service","quantity":2,"unit_price":"50.00"}]
}

8. Invoices

POST /api/v1/invoices {
  "customer_id":1, "issue_date":"2026-09-25", "due_date":"2026-10-02",
  "discount_type":"percent","discount_value":10,
  "items":[
    {"description":"Design","quantity":1,"unit_price":"500.00","tax_rate":10},
    {"product_id":3,"description":"Hosting","quantity":2,"unit_price":"50.00"}
  ]
}

Invoices use InvoiceService + InvoiceCalculator — line subtotal → line discount → invoice discount → tax (inclusive/exclusive per company). Only draft invoices are update/delete-able.

PUT /api/v1/invoices/{id}  # draft only
DELETE /api/v1/invoices/{id} # draft only -> 422 otherwise

9. Payments

POST /api/v1/payments {
  "customer_id":1,"bank_account_id":2,"amount":5000,"payment_date":"2026-09-25",
  "method":"bank_transfer",
  "allocations":[{"invoice_id":10,"amount":5000}]
}
POST /api/v1/payments/{id}/void { "reason":"Duplicate" }

Unallocated amount becomes customer credit. Void reverses ledger via LedgerPostingService::reverse.

10. Expenses & Vendors

POST /api/v1/vendors { "name":"Acme Supplies" }
POST /api/v1/expenses {
  "expense_category_id":1,"bank_account_id":1,"amount":2500,
  "date":"2026-09-25","description":"Office supplies"
}
POST /api/v1/expenses/{id}/void { "reason":"Wrong amount" }

11. Transactions (ledger)

GET /api/v1/transactions?type=transfer
POST /api/v1/transactions/transfer { "from_account_id":1,"to_account_id":2,"amount":2000,"date":"2026-09-25" }
POST /api/v1/transactions/income { "bank_account_id":1,"ledger_account_id":5,"amount":10000,"date":"2026-09-25" }
POST /api/v1/transactions/adjustment { "bank_account_id":1,"amount":-500,"date":"2026-09-25","description":"Correction" }

All writes go through LedgerPostingService — the single ledger writer.

12. Finance SaaS integrations — two-way sync (10 providers)

Portal → Settings → Integrations (owner only). Each provider is managed as an Integration row (company_id + provider unique). Two-way sync means push (local → provider) and pull (provider → local).

#ProviderLabelAPI baseDocs
1xeroXerohttps://api.xero.com/api.xro/2.0docs
2quickbooksQuickBooks Onlinehttps://quickbooks.api.intuit.com/v3docs
3freshbooksFreshBookshttps://api.freshbooks.comdocs
4hubspotHubSpothttps://api.hubapi.comdocs
5zoho_booksZoho Bookshttps://www.zohoapis.com/books/v3docs
6waveWavehttps://gql.waveapps.com/graphql/publicdocs
7sageSage Business Cloudhttps://api.sage.comdocs
8netsuiteOracle NetSuitehttps://{accountId}.suitetalk.api.netsuite.com/services/restdocs
9myobMYOBhttps://api.myob.com/accountrightdocs
10kashooKashoohttps://api.kashoo.comdocs

Connect

POST /portal/settings/integrations { "provider":"xero","access_token":"...","refresh_token":"...","external_id":"org_123" }

Sync

POST /portal/settings/integrations/{id}/sync { "entity_type":"customer","entity_id":12,"direction":"push" }
POST /portal/settings/integrations/{id}/sync { "entity_type":"customer","entity_id":12,"direction":"pull" }
GET  /portal/settings/integrations/{id}/logs  # last 50 SyncLogs

SyncService::push formats the local entity for the provider, simulates Http::post(provider.apiBaseUrl()), stores an integration_sync_logs row and flips status to connected/error. pull fetches remote and upserts locally (e.g. customer). Replace the stubbed fetchFromProvider/formatForProvider with real HTTP per provider when credentials are available.

All syncs are company-scoped — pushing entity #5 from company A cannot read company B's row (verified by test sync isolates by company).

13. Errors & rate limiting

401 { "message": "Unauthenticated. Invalid API token." }
422 { "message": "The given data was invalid.", "errors": { "name": ["..."] } }
403 Forbidden (portal gates)
429 Too Many Requests (throttle)

API routes are throttled by Laravel's api limiter. Portal routes use customer + company middleware and Permission::ManageCompany for API app / integration management.

14. Test cases

Feature tests in tests/Feature/ApiApplicationTest.php, ApiEndpointTest.php, IntegrationTest.php — run via php artisan test.

TestWhat it provesFile
owner can create and revoke an API applicationToken generated with gb_ prefix, hash stored, revoke deletesApiApplicationTest
staff cannot manage API applications403 via GateApiApplicationTest
API token authenticates and scopes to company401 without/bad/expired, 200 with good token, /meApiApplicationTest
API token cannot access another companyTenant isolation via CompanyContextApiApplicationTest
can CRUD customers via APIPOST/GET/PUT/DELETE + search/paginationApiEndpointTest
can CRUD products via APIProduct create/showApiEndpointTest
can create invoice via API with calculatorInvoiceService totals = 10000 centsApiEndpointTest
validates invoice payload422 on missing customer_id/itemsApiEndpointTest
can record payment and transfer via APIPayment + ledger transfer both via servicesApiEndpointTest
expenses API creates and voidsExpense record + void reversalApiEndpointTest
owner can connect and disconnect integrationsXero connect, update, disconnectIntegrationTest
staff cannot manage integrations403IntegrationTest
all ten providers are availableCount 10, labels, base URLs, two-way flagIntegrationTest
sync service pushes and pulls with logsPush/pull create integration_sync_logs, update last_sync_atIntegrationTest
sync isolates by companyPushing other company's entity failsIntegrationTest
logs endpoint returns JSONGET logs 200IntegrationTest
php artisan test --filter=Api
php artisan test --filter=Integration
php artisan test  # all 368 + new tests

15. Examples (curl)

# Create customer
curl -H "Authorization: Bearer gb_xxx" -H "Content-Type: application/json" \
  -d '{"name":"Acme","email":"acme@example.com"}' https://app.example.com/api/v1/customers

# List invoices
curl -H "Authorization: Bearer gb_xxx" "https://app.example.com/api/v1/invoices?page=1&per_page=20"

# Create invoice with tax
curl -H "Authorization: Bearer gb_xxx" -H "Content-Type: application/json" -d '{
  "customer_id":1,"issue_date":"2026-09-25","due_date":"2026-10-02",
  "items":[{"description":"Consulting","quantity":10,"unit_price":"75.00","tax_rate":10}]
}' https://app.example.com/api/v1/invoices

# Void payment
curl -X POST -H "Authorization: Bearer gb_xxx" -H "Content-Type: application/json" \
  -d '{"reason":"Duplicate entry"}' https://app.example.com/api/v1/payments/5/void