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

Xero Php Oauth2 Laravel Package

xeroapi/xero-php-oauth2

A PHP SDK for the Xero API using OAuth 2.0. It provides ready-made classes and models for accounting, payroll, and other Xero endpoints, handling authentication, token refresh, and requests so you can integrate Xero into Laravel or any PHP app quickly.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. composer require xeroapi/xero-php-oauth2 — most Laravel apps pair it with the xeroapi/xero-laravel wrapper, which supplies config publishing, a token model, and a ready-made StorageInterface. Install the SDK alone only if you already own the token lifecycle.
  2. Register an app at https://developer.xero.com/app/manage → grab Client ID, Client Secret, and set the redirect URI to something like https://app.test/xero/callback. Put them in config/services.php (or config/xero.php if using the wrapper), never in code.
  3. Seed .env with XERO_CLIENT_ID, XERO_CLIENT_SECRET, XERO_REDIRECT_URI, and XERO_SCOPES — start with offline_access accounting.transactions accounting.contacts and add scopes as you need them. Scopes are baked into the refresh token, so widening them later forces every tenant to re-consent.
  4. Run the OAuth2 authorization-code flow once per tenant, persist the token, and note the xero-tenant-id returned by /connections.
  5. First use case: pull the last 30 days of invoices for one tenant.
$config = \XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()
    ->setClientId(config('services.xero.client_id'))
    ->setClientSecret(config('services.xero.client_secret'))
    ->setAccessToken($token->access_token);

$api = new \XeroAPI\XeroPHP\Api\AccountingApi(new \GuzzleHttp\Client(), $config);
$invoices = $api->getInvoices($tenantId, new \DateTime('2024-01-01'), null, ['AUTHORISED'], null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 1);

Look first at the generated Api/*Api.php method signatures (they are long positional parameter lists) and the Models/Accounting classes — that is the surface you will actually code against.

Implementation Patterns

Wrap the generated APIs. Never call AccountingApi from a controller or job directly. Create thin repositories (XeroInvoiceRepository, XeroContactRepository) that take the tenant ID and return your own DTOs or Eloquent models. The 16.2.0 release regenerates models from OAS 19.0.0 — every upstream spec bump can shift a signature or add a nullable field, and a facade layer is the only thing that stops that from rippling across your app.

Token handling is your job. Implement XeroAPI\XeroPHP\StorageInterface against an Eloquent xero_tokens table (columns: tenant_id, access_token, refresh_token, expires_at, scopes). Before every request, refresh if expires_at is within ~2 minutes. Tokens live 30 minutes and refresh tokens rotate, so wrap refresh in Cache::lock("xero-refresh:{$tenantId}") — two concurrent jobs refreshing with the same refresh token will invalidate each other and force re-consent.

Multi-tenant fan-out. One Xero "connection" equals one organisation. Store connections per user, then iterate:

foreach ($user->xeroConnections as $connection) {
    dispatch(new SyncXeroInvoices($connection->tenant_id));
}

Never share a Configuration instance across tenants in the same request — the tenant ID is passed per call but the access token is not.

Writes belong in queued jobs. Set $tries = 5, backoff() returning [10, 60, 300], and treat HTTP 429 specially (see gotchas). Use ShouldBeUnique keyed on the Xero document ID for creates so a retried job doesn't double-post an invoice.

Read efficiently with delta sync. For any list endpoint take advantage of the If-Modified-Since header and where date filtering rather than pulling everything nightly:

$config->setDefaultHeaders(['If-Modified-Since' => $lastSync->toRfc7231String()]);

Persist last_successful_sync_at per tenant/entity and resume from there. Paginate with the page parameter (most accounting endpoints cap at 100 records per page); loop until a page returns fewer than the max.

Prefer summarised endpoints. getReportBalanceSheet, getReportProfitAndLoss, and the batch payment summaries are one call versus dozens — worth checking the API reference before hand-rolling aggregation.

Webhooks over polling. Register a webhook via /webhooks, verify the x-xero-signature HMAC with your webhook key, return 200 immediately, and push processing onto the queue. Webhooks only tell you that something changed plus the resource ID — you still fetch the entity. Combine with the delta sync as a safety net for missed deliveries.

Integration testing. Point at https://api.xero.com/api.xro/2.0 for sandbox orgs by keeping base paths in config; use Laravel's Http::fake() against captured payloads rather than hitting Xero in CI.

Gotchas and Tips

  • The 16.2.0 / OAS 19.0.0 relationship matters. This SDK is 100% generated. A minor release that merely says "OAS: 19.0.0" means the models and API classes were regenerated against a newer Xero spec. That can silently add enum constants, mark previously required fields nullable, or add fields to toArray() output. Diff the Models/ and Api/ directories between 16.1.0 and 16.2.0 before merging, and run your integration suite against a sandbox tenant — your JSON fixtures may need regenerating.
  • Pin the version. "xeroapi/xero-php-oauth2": "16.2.*" rather than ^16.0, and read the CHANGELOG. Major bumps have historically renamed/removed model setters.
  • Rate limits are strict and per-tenant: 60 calls/minute, 5 concurrent, 5,000/day. On HTTP 429 read the Retry-After header and sleep for it; don't use a fixed backoff. On HTTP 503 back off harder — Xero throttles aggressively during peak.
  • ApiException carries the real message. Always catch (\XeroAPI\XeroPHP\ApiException $e) and log $e->getResponseBody() — a bare "400 Bad Request" from Guzzle tells you nothing, but the body contains ValidationErrors with Message and ElementPath pointing at the offending field.
  • Dates: endpoints expect DateTimeInterface or Y-m-d strings, not Carbon's default serialisation. Carbon::parse($x)->setTime(0,0) before passing to avoid timezone drift shifting an invoice into the wrong day.
  • Line items need a tax type and a positive quantity. Negative quantities and missing TaxType on an ACCREC are the two most common cause of 400s. Compute LineAmount yourself and set LineAmountTypes explicitly (Exclusive/Inclusive/NoTax) — the default is not always what you expect.
  • Empty vs null: generated getters return [] for empty collections and null for absent scalars. Guard with ?? null and count() checks rather than assuming presence.
  • Separate API classes and scopes exist for Projects, Payroll AU/UK/NZ, Practice Manager, Assets, and Files. Requesting an accounting scope does not grant payroll access, and a missing scope surfaces as a 401, not a 403 — check your token's stored scopes first when debugging auth failures.
  • Don't edit vendor/xeroapi/. If you need to patch generated behaviour, subclass the API/model in your own namespace and bind it, otherwise composer update will overwrite you.
  • Extension point: Configuration::setDefaultHeaders(), a custom Guzzle handler stack (for logging and metrics), and the StorageInterface are the three sanctioned hooks. Logging middleware on the Guzzle client is the fastest way to diagnose request/response issues in production without redeploying.
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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core