sudiptpa / xero-php-sdk
A fluent, framework-agnostic Xero PHP SDK for PHP 8.2 to 8.5 with rich models and a clean API.
Fund package maintenance!
Requires
- php: >=8.2 <8.6
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
This package is auto-updated.
Last update: 2026-04-07 04:06:07 UTC
README
If this package has been useful to you, GitHub Sponsors is a simple way to support ongoing maintenance, improvements, and future releases.
A fluent, framework-agnostic Xero SDK for PHP 8.2 to 8.5 with rich models, a fluent API, and no runtime dependencies.
- Rich models for reads and writes
- Fluent request flows across Xero families
- Aligned to the official Xero docs
Why We Built It
- Build Xero integrations with rich models instead of raw payload arrays.
- Use one consistent API across Accounting, Files, Assets, Projects, Payroll, Finance, App Store, Identity, and Webhooks.
- Keep integration code readable in plain PHP or inside any framework.
This package is built for developers who want a modern Xero PHP SDK with a clear API for accounting, payroll, OAuth2, files, webhooks, and tenant-aware integrations.
Installation
composer require sudiptpa/xero-php-sdk
Runtime Notes:
- PHP 8.2 to 8.5
ext-jsonfor JSON request and response handlingext-curlfor the built-in native transport
If ext-curl is installed, Xero::withAccessToken(...) uses the built-in native transport by default.
If ext-curl is not installed, requests throw a transport exception when sent. In that case, supply your own transport such as a Guzzle-based transport.
Custom Transport
If you want to use a custom transport, pass it explicitly.
Guzzle Example
use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Exception\GuzzleException; use Sujip\Xero\Exceptions\TransportException; use Sujip\Xero\Http\Request; use Sujip\Xero\Http\Response; use Sujip\Xero\Http\Transport; use Sujip\Xero\Xero; final class GuzzleTransport implements Transport { public function __construct( private readonly GuzzleClient $client = new GuzzleClient() ) { } public function send(Request $request): Response { try { $response = $this->client->request($request->method, $request->url(), [ 'headers' => $request->headers, 'json' => $request->json, 'body' => $request->body, ]); } catch (GuzzleException $exception) { throw new TransportException($exception->getMessage(), previous: $exception); } return new Response( $response->getStatusCode(), array_map( static fn (array $values): string => $values[0] ?? '', $response->getHeaders() ), (string) $response->getBody(), ); } } $xero = Xero::withAccessToken('token', new GuzzleTransport()) ->tenant('tenant-id');
Quick Start
Most Xero integrations follow the same path:
- build an authorization URL
- exchange the callback code for a token
- list available tenant connections
- choose one tenant
- make your first API call
This is the shortest useful path:
use Sujip\Xero\Auth\InMemoryTokenRepository; use Sujip\Xero\Xero; $manager = Xero::oauth2( clientId: 'client-id', clientSecret: 'client-secret', redirectUri: 'https://example.com/xero/callback', )->manager(new InMemoryTokenRepository()); $url = $manager->authorizationUrl( scopes: ['openid', 'offline_access', 'accounting.contacts'], state: 'csrf-token', );
When Xero redirects back:
$token = $manager->exchange($code); $tenants = $manager->connections(); $connected = $manager->connectTenant($tenants[0]->tenantId); $contacts = $connected->tenant() ->accounting() ->contacts() ->page(1) ->get();
Use tenant() for the fluent tenant-scoped path. getClient() is also available if you prefer a more explicit accessor.
If you already know the tenant id, exchangeAndConnect() is the shorter path:
$connected = $manager->exchangeAndConnect($code, 'tenant-id');
Usage
use Sujip\Xero\Xero; $xero = Xero::withAccessToken('token') ->tenant('tenant-id'); $contacts = $xero->accounting() ->contacts() ->where('Name.Contains(:name)', name: 'Acme') ->orderBy('Name') ->page(1) ->get();
$page = $xero->accounting() ->contacts() ->paginate(page: 2);
use Sujip\Xero\Accounting\Invoice\Invoice; use Sujip\Xero\Accounting\Contact\Contact; use Sujip\Xero\Accounting\Invoice\LineItem; $invoice = $xero->accounting() ->invoices() ->create() ->using( (new Invoice()) ->setType('ACCREC') ->setStatus('DRAFT') ->setContact( (new Contact()) ->setContactID('contact-id') ) ->setReference('PO-1001') ->addLineItem( (new LineItem()) ->setDescription('Consulting') ->setQuantity(2) ->setUnitAmount(150) ) ) ->save();
use Sujip\Xero\Accounting\Account\Account; use Sujip\Xero\Accounting\Payment\Payment; $payment = $xero->accounting() ->payments() ->create() ->using( (new Payment()) ->setInvoiceID('invoice-id') ->setAccount( (new Account()) ->setAccountID('account-id') ) ->setDate('2026-03-25') ->setAmount(150) ->setReference('PAY-1001') ) ->save();
use Sujip\Xero\Accounting\Contact\Contact; $updated = $xero->accounting() ->contacts() ->update('contact-id') ->using( (new Contact()) ->setContactID('contact-id') ->setName('Acme Holdings Pty Ltd') ) ->save();
$attachment = $xero->accounting() ->invoices() ->attachments('invoice-id') ->upload('invoice.pdf', $pdfBinary) ->mimeType('application/pdf') ->includeOnline() ->save();
$file = $xero->files() ->upload('contract.pdf', $binary) ->mimeType('application/pdf') ->toFolder('folder-id') ->save(); $fileName = $file->getName();
$folder = $xero->files() ->folders() ->inbox(); $isInbox = $folder?->getIsInbox();
$files = $xero->files() ->forObject('invoice-id') ->get();
$assets = $xero->assets() ->status('registered') ->orderBy('AssetName') ->filterBy('MacBook') ->get(); $assetName = $assets->first()?->getAssetName();
$project = $xero->projects() ->create() ->title('Website rebuild') ->contact('contact-id') ->estimateAmount(1200) ->save(); $projectId = $project->getProjectID();
$entries = $xero->projects() ->timeEntries('project-id') ->user('user-id') ->task('task-id') ->states('INPROGRESS') ->get();
$employees = $xero->payroll() ->au() ->employees() ->page(1) ->get();
$leave = $xero->payroll() ->au() ->leaveApplications() ->create() ->employee('employee-id') ->leaveType('leave-type-id') ->title('Annual Leave') ->startDate('2026-04-01') ->endDate('2026-04-02') ->save();
$timesheet = $xero->payroll() ->nz() ->timesheets() ->create() ->employee('employee-id') ->startDate('2026-03-23') ->endDate('2026-03-29') ->status('DRAFT') ->save();
$balances = $xero->payroll() ->uk() ->employees() ->find('employee-id') ?->leaveBalances();
$balanceSheet = $xero->finance() ->statements() ->balanceSheet(new DateTimeImmutable('2026-03-31'));
$subscription = $xero->appStore() ->subscriptions() ->find('subscription-id');
$connections = Xero::withAccessToken($token) ->identity() ->connections() ->get();
$verifier = Xero::webhookVerifier($signingKey); $verifier->assertValid($rawPayload, $signatureHeader); $webhook = $verifier->parse($rawPayload);
Granular Scopes
-
Apps created on or after 2 March 2026 use granular scopes
-
Apps created before 2 March 2026 can begin requesting granular scopes from April 2026
-
Existing apps have until September 2027 to complete migration from broad scopes
-
Ask only for the scopes the integration actually uses.
-
Prefer
.readscopes for read-only jobs. -
Expect
401insufficient-scope responses if an app is missing a required scope.
Identity And Tenants
Use identity()->connections() to discover which tenants a token can access. Use tenant(...) when you make tenant-scoped API calls such as Accounting, Files, Projects, Assets, Finance, and Payroll requests.
Auth Flow
use Sujip\Xero\Auth\InMemoryTokenRepository; use Sujip\Xero\Xero; $manager = Xero::oauth2( clientId: 'client-id', clientSecret: 'client-secret', redirectUri: 'https://example.com/xero/callback', )->manager(new InMemoryTokenRepository()); $url = $manager->authorizationUrl( scopes: ['openid', 'offline_access', 'accounting.contacts'], state: 'csrf-token', );
After callback:
$manager->exchange($code); $connected = $manager->connectTenant('tenant-id'); $xero = $connected->tenant();
See Auth for PKCE, refresh, tenant selection, and custom connection flows.
Supported APIs
- Accounting
- Files
- Assets
- Projects
- Payroll AU
- Payroll NZ
- Payroll UK
- Finance
- App Store
- Identity
- Webhooks
Documentation
- Architecture
- Auth
- Accounting
- Accounting Coverage
- Files
- Assets
- Projects
- Payroll AU
- Payroll NZ
- Payroll UK
- Finance
- App Store
- Webhooks
Contributing
If you want to help, start with: