Search by

sudiptpa / xero-php-sdk

sudiptpa

A fluent, framework-agnostic Xero PHP SDK for PHP 8.2 to 8.5 with rich models and a clean API.

Package info

github.com/sudiptpa/xero-php-sdk

pkg:composer/sudiptpa/xero-php-sdk

Fund package maintenance!

sudiptpa

Statistics

Installs: 5 071

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v4.0.0 2026-09-16 16:31 UTC

README

xero-php-sdk

PHP 8.2-8.5 Tests Latest Version Total Downloads Release Framework Agnostic License: MIT

Sponsor

If this package saves you time, GitHub Sponsors is a simple way to support it.

A fluent, framework-agnostic Xero SDK for PHP 8.2 to 8.5. No runtime dependencies. Drop it into Laravel, Symfony, or a plain PHP project; it runs anywhere PHP runs.

  • Typed models and array responses for Xero resources
  • Fluent builders for reads and writes
  • Covers Accounting, Payroll, Files, Assets, Projects, Finance, App Store, Identity, and Webhooks

Installation

composer require sudiptpa/xero-php-sdk

Requires:

  • PHP 8.2 to 8.5
  • ext-json
  • ext-curl (for the built-in transport)

If ext-curl is not available, supply your own transport (see Custom transport).

Quick start

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 Xero redirects back with a code:

$token = $manager->exchange($code);
$tenants = $manager->connections();

$connected = $manager->connectTenant($tenants[0]->getTenantId());

$contacts = $connected->tenant()
    ->accounting()
    ->contacts()
    ->page(1)
    ->get();

Call tenant() to get a client scoped to that tenant. getClient() works too.

If you already know the tenant id:

$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);

Custom transport

Implement the Transport interface to use a different HTTP client.

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');

Scopes

  • Apps created on or after 2 March 2026 must use granular scopes
  • Apps created before 2 March 2026 can start requesting granular scopes from April 2026
  • All apps must migrate off broad scopes by September 2027
  • Request only the scopes the integration actually uses
  • Use .read scopes for read-only work
  • A missing scope returns a 401 insufficient-scope response

Tenants

Use identity()->connections() to list which tenants a token can access. Make tenant-scoped API calls (Accounting, Files, Projects, Assets, Finance, Payroll) only after calling tenant(...).

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, token refresh, tenant selection, and custom connection flows.

Supported APIs

  • Accounting
  • Files
  • Assets
  • Projects
  • Payroll AU
  • Payroll NZ
  • Payroll UK
  • Finance
  • App Store
  • Identity
  • Webhooks

Stability

This SDK is maintained against the official Xero OpenAPI specs and ships with full test coverage, static analysis, and formatting checks. Public API corrections that can affect existing callers are reserved for major releases. Patch and minor releases should remain backward compatible.

Documentation

Contributing

See Contributing, Security policy, and Changelog.