PHPackages                             clinically/laravel-halaxy - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. clinically/laravel-halaxy

ActiveLibrary[API Development](/categories/api)

clinically/laravel-halaxy
=========================

Laravel SDK for the Halaxy FHIR R4B healthcare API

v1.2.1(today)00MITPHPPHP ^8.5

Since Aug 1Pushed todayCompare

[ Source](https://github.com/clinically-au/laravel-halaxy)[ Packagist](https://packagist.org/packages/clinically/laravel-halaxy)[ Docs](https://github.com/clinically-au/laravel-halaxy)[ RSS](/packages/clinically-laravel-halaxy/feed)WikiDiscussions main Synced today

READMEChangelog (2)Dependencies (10)Versions (3)Used By (0)

clinically/laravel-halaxy
=========================

[](#clinicallylaravel-halaxy)

A Laravel SDK for the [Halaxy](https://developers.halaxy.com) FHIR R4B healthcare API.

Important

This is an independent, unofficial project. It is not affiliated with, endorsed by, or supported by Halaxy. Halaxy is a trademark of its respective owner.

Features
--------

[](#features)

- Fluent resource API for every documented Halaxy endpoint
- FHIR query builder with pagination (`paginate()`, lazy `all()`)
- Multi-tenant support with per-tenant OAuth token caching
- AU/EU region switching
- Webhook handling via `spatie/laravel-webhook-client` with typed events
- Typed exceptions for authentication, validation, rate-limit, and server errors

Requirements
------------

[](#requirements)

- PHP 8.5+
- Laravel 13+

Installation
------------

[](#installation)

```
composer require clinically/laravel-halaxy
```

Publish the configuration:

```
php artisan vendor:publish --tag="halaxy-config"
```

Set your credentials in `.env` (created per practice in Halaxy's developer settings):

```
HALAXY_CLIENT_ID=your-client-id
HALAXY_CLIENT_SECRET=your-client-secret
HALAXY_REGION=au  # au or eu
```

Usage
-----

[](#usage)

### Patients

[](#patients)

```
use Clinically\Halaxy\Facades\Halaxy;

// Find, list, create
$patient = Halaxy::patients()->find('123456')->json();
$bundle = Halaxy::patients()->list(['_count' => 30]);

$created = Halaxy::patients()->create([
    'name' => [['use' => 'official', 'given' => ['John'], 'family' => 'Doe']],
    'birthDate' => '1990-01-15',
]);

// Partial update (JSON merge-patch)
Halaxy::patients()->update('123456', [
    'telecom' => [['system' => 'phone', 'value' => '0400000000', 'use' => 'mobile']],
]);

// Full replace (PUT) — destructive: writable properties omitted from the
// payload are REMOVED from the profile (only file attachments survive).
// Always send the complete desired state.
Halaxy::patients()->replace('123456', [
    'name' => [['use' => 'official', 'given' => ['John'], 'family' => 'Doe']],
    'birthDate' => '1990-01-15',
    'telecom' => [['system' => 'phone', 'value' => '0400000000', 'use' => 'mobile']],
]);

// Export references for every patient in the practice
$references = Halaxy::patients()->exportIds()->json();
```

### Query Builder

[](#query-builder)

```
$paginator = Halaxy::patients()
    ->query()
    ->where('family', 'Doe')
    ->where('_lastUpdated', 'gt', '2024-01-01')
    ->orderBy('family')
    ->paginate(50);

foreach ($paginator->items() as $patient) {
    // current page
}

foreach ($paginator->all() as $patient) {
    // lazily fetches every page
}
```

### Appointments

[](#appointments)

Booking uses Halaxy's `$book` operation, which expects a FHIR **Parameters**resource (not a bare Appointment) and creates related resources (invoice, clinical note) as part of the booking:

```
// Find available times (start, end, and duration are required)
$available = Halaxy::appointments()->findAvailable([
    'start' => '2026-02-15T09:00:00+11:00',
    'end' => '2026-02-22T17:00:00+11:00',
    'duration' => '30',
    'show' => 'first-available',
]);

// Book
$booked = Halaxy::appointments()->book([
    'parameter' => [
        [
            'name' => 'appt-resource',
            'resource' => [
                'resourceType' => 'Appointment',
                'start' => '2026-02-15T09:00:00+11:00',
                'end' => '2026-02-15T09:30:00+11:00',
                'minutesDuration' => 30,
                'participant' => [
                    ['actor' => ['type' => 'PractitionerRole', 'reference' => 'https://au-api.halaxy.com/main/PractitionerRole/PR-123']],
                ],
            ],
        ],
        ['name' => 'patient-id', 'valueReference' => ['type' => 'Patient', 'reference' => 'https://au-api.halaxy.com/main/Patient/123456']],
        ['name' => 'healthcare-service-id', 'valueReference' => ['type' => 'HealthcareService', 'reference' => 'https://au-api.halaxy.com/main/HealthcareService/789']],
        ['name' => 'location-type', 'valueCode' => 'clinic'],
        ['name' => 'status', 'valueCode' => 'booked'],
    ],
]);

// Update (merge-patch): description, start/end, comment, participant, ...
Halaxy::appointments()->update($appointmentId, ['comment' => 'Running late']);
```

Appointments have no writable `status` field. Cancellation is done by patching the patient participant's `modifierExtension`:

```
Halaxy::appointments()->update($appointmentId, [
    'participant' => [[
        'actor' => ['type' => 'Patient', 'reference' => "https://au-api.halaxy.com/main/Patient/{$patientId}"],
        'modifierExtension' => [[
            'url' => 'https://terminology.halaxy.com/StructureDefinition/appointment-participant-status',
            'valueCoding' => [
                'system' => 'https://au-api.halaxy.com/presets/CodeSystem/appointment-participant-status',
                'code' => 'cancelled (no charge)',
                'display' => 'cancelled (no charge)',
            ],
        ]],
    ]],
]);
```

### Referrals

[](#referrals)

Halaxy referrals use Halaxy-specific property names rather than the generic FHIR ServiceRequest names. A Coverage record must already exist. Use the typed payload to produce `coverage`, `created`, `active`, `comment`, and `attachments`:

```
use Clinically\Halaxy\DTOs\ReferralAttachment;
use Clinically\Halaxy\DTOs\ReferralPayload;

$referral = Halaxy::referrals()->create(new ReferralPayload(
    coverageReference: "Coverage/{$coverageId}",
    subjectReference: "Patient/{$patientId}",
    requesterReference: "PractitionerRole/{$requesterId}",
    created: new DateTimeImmutable('now'),
    comment: 'Specialist review requested',
    attachments: [
        new ReferralAttachment('application/pdf', $base64Data, 'referral.pdf'),
    ],
));
```

Halaxy's referral PATCH endpoint only appends attachments. It does not update or replace other referral properties:

```
Halaxy::referrals()->addAttachments(
    $referralId,
    new ReferralAttachment('application/pdf', $base64Data, 'additional.pdf'),
);
```

### Schedules and Slots

[](#schedules-and-slots)

```
$schedules = Halaxy::schedules()->list();

// Generate slots for one schedule via its $generate operation
Halaxy::schedules()->generateSlots('33001', [
    'period' => [
        'start' => '2026-02-01T09:00:00+11:00',
        'end' => '2026-02-08T17:00:00+11:00', // at least 24h after start
    ],
]);

$slots = Halaxy::slots()->list(['schedule' => 'Schedule/33001']);
```

### Region Switching

[](#region-switching)

```
$response = Halaxy::region('eu')->patients()->find('789');
```

### Multi-Tenant Usage

[](#multi-tenant-usage)

Each tenant (e.g. clinic) can use its own Halaxy credentials with isolated OAuth token caching:

```
$halaxy = Halaxy::for(
    tenantId: $clinic->id,
    clientId: $clinic->halaxy_client_id,
    clientSecret: $clinic->halaxy_client_secret,
    region: 'au',
);

$patients = $halaxy->patients()->list();

// One-off credentials (not cached by tenant)
$halaxy = Halaxy::withCredentials(clientId: '...', clientSecret: '...', region: 'eu');

// Cache management
Halaxy::forgetTenant($clinic->id);
Halaxy::flush();
```

### Direct Client Access

[](#direct-client-access)

```
$client = Halaxy::getClient();
$response = $client->get('Patient', ['family' => 'Doe']);
```

Available Resources
-------------------

[](#available-resources)

GroupMethodOperationsPeople`patients()`find, list, create, update, replace, exportIdsPeople`practitioners()`find, list, createPeople`practitionerRoles()`find, list, createPeople`organizations()`find, list, createScheduling`appointments()`find, list, book/create, update, findAvailableScheduling`schedules()`find, list, create, generateSlotsScheduling`slots()`find, listScheduling`healthcareServices()`find, listFinancial`chargeItemDefinitions()`find, listFinancial`coverages()`find, list, create, updateFinancial`invoices()`find, listFinancial`invoiceLines()`find, listFinancial`paymentTransactions()`find, listFinancial`referrals()`find, list, create, addAttachmentsFinancial`referralDefinitions()`find, listClinical`documentReferences()`createFoundations`capabilities()`getFoundations`searchParameters()`listThe SDK deliberately exposes only the operations Halaxy documents — there is no `delete()` anywhere because the Halaxy API has no delete endpoint, and `replace()` (PUT) exists only on patients.

Halaxy API Quirks
-----------------

[](#halaxy-api-quirks)

Verified against the live API — worth knowing before you integrate:

- **Nothing can be deleted.** No resource has a delete endpoint. Records you create (patients, contacts, notes) persist until removed via the Halaxy UI.
- **Patient `identifier` and `active` are read-only.** Identifiers sent on create are silently dropped, so you cannot tag patients for later lookup; store Halaxy patient IDs on your side. Writes to `active` are silently ignored — archiving is UI-only.
- **PUT replace is destructive.** Writable properties omitted from a `replace()` payload are removed. Send the complete desired state.
- **Appointment status is not directly writable.** Book with the `status`parameter; cancel via the participant `modifierExtension` (see above).
- **Polymorphic search parameters need `Type/id` values.** e.g. `recipient=Patient/123` — a bare ID returns HTTP 422. Some documented patient-scoping parameters are silently ignored by the server; verify filters against a control query before trusting them.
- **A `User-Agent` header is mandatory.** Halaxy's gateway rejects requests without one (HTTP 403). The SDK always sends `halaxy.user_agent`.

Webhooks
--------

[](#webhooks)

The SDK integrates `spatie/laravel-webhook-client` to receive Halaxy webhooks. Webhook handling is disabled by default and must be explicitly enabled with a signing secret. Requests fail closed if the secret is missing.

Halaxy webhook payloads identify *which resource* changed but not *what happened* to it, so each event type gets its own endpoint. When creating each webhook in Halaxy (Settings &gt; Integrations &gt; Webhooks), point it at the URL matching its event:

Halaxy eventEndpointPatient Create`https://your-app/webhooks/halaxy/patient-created`Patient Update`https://your-app/webhooks/halaxy/patient-updated`Appointment Create`https://your-app/webhooks/halaxy/appointment-created`Appointment Update`https://your-app/webhooks/halaxy/appointment-updated`Appointment Delete`https://your-app/webhooks/halaxy/appointment-deleted`Invoice Create`https://your-app/webhooks/halaxy/invoice-created`Invoice Update`https://your-app/webhooks/halaxy/invoice-updated`Invoice Delete`https://your-app/webhooks/halaxy/invoice-deleted`Set the webhook's "Authentication Header" in Halaxy to your `HALAXY_WEBHOOK_SECRET` value.

```
HALAXY_WEBHOOKS_ENABLED=true
HALAXY_WEBHOOK_SECRET=your-webhook-secret
HALAXY_WEBHOOK_PATH=webhooks/halaxy
```

Warning

`HALAXY_WEBHOOKS_ENABLED=true` without `HALAXY_WEBHOOK_SECRET` rejects every webhook request. Configure the same secret as the webhook's Authentication Header in Halaxy before enabling the endpoints.

Exclude the webhook routes from CSRF verification in `bootstrap/app.php`:

```
->withMiddleware(function (Middleware $middleware): void {
    $middleware->validateCsrfTokens(except: ['webhooks/halaxy', 'webhooks/halaxy/*']);
})
```

Run the spatie migration to create the `webhook_calls` table:

```
php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-migrations"
php artisan migrate
```

Each endpoint dispatches its typed event — `PatientCreated`, `PatientUpdated`, `AppointmentCreated`, `AppointmentUpdated`, `AppointmentDeleted`, `InvoiceCreated`, `InvoiceUpdated`, `InvoiceDeleted` — exposing `resourceReference`, `timestamp`, `webhookCall`, and an ID accessor (e.g. `patientId()`). Requests to the base path dispatch the generic `HalaxyWebhookReceived`.

### Multi-tenant hosts

[](#multi-tenant-hosts)

Disable auto-registration and register the routes inside your own tenant-scoped group:

```
HALAXY_WEBHOOK_ROUTES=false
```

```
// e.g. routes/tenant-webhooks.php, grouped under {tenant} + tenant middleware
use Clinically\Halaxy\Webhooks\HalaxyWebhookRoutes;

HalaxyWebhookRoutes::register('halaxy');
```

Point each Halaxy webhook at the tenant URL, e.g. `https://your-app/app/{tenant}/webhooks/halaxy/appointment-created`.

For per-tenant secrets, substitute your own validator (and any other component) in `config/halaxy.php`:

```
'webhooks' => [
    // ...
    'signature_validator' => App\Webhooks\TenantHalaxySignatureValidator::class,
],
```

Entries named `halaxy`/`halaxy-*` in a host's published `webhook-client.php`are superseded by the package's entries — put overrides in `config/halaxy.php`'s `webhooks` block instead.

Error Handling
--------------

[](#error-handling)

```
use Clinically\Halaxy\Exceptions\AuthenticationException;
use Clinically\Halaxy\Exceptions\NotFoundException;
use Clinically\Halaxy\Exceptions\RateLimitException;
use Clinically\Halaxy\Exceptions\ServerException;
use Clinically\Halaxy\Exceptions\ValidationException;

try {
    $response = Halaxy::patients()->find('invalid-id');
} catch (NotFoundException $e) {
    // 404
} catch (ValidationException $e) {
    // 400/422 — $e->operationOutcome holds the FHIR OperationOutcome issues
} catch (AuthenticationException $e) {
    // 401/403 — message includes the OAuth error detail
} catch (RateLimitException $e) {
    $retryAfter = $e->retryAfter;
}
```

Testing
-------

[](#testing)

```
composer test              # Unit + Feature suites (HTTP is faked)
composer test:unit
composer test:feature
composer analyse           # PHPStan
composer lint              # Pint
```

### Integration tests (live API)

[](#integration-tests-live-api)

The `Integration` suite exercises the real Halaxy API and is excluded from `composer test`. To run it, copy `.env.example` to `.env`, add credentials for a practice with **curated test patients**, and pin their IDs:

```
HALAXY_CLIENT_ID=...
HALAXY_CLIENT_SECRET=...
HALAXY_TEST_PATIENT_IDS=111,222   # first = "Test, Patient", second = "Test, Another"
HALAXY_ALLOW_WRITES=false         # true enables write tests (see .env.example)
```

```
composer test:integration
```

Tests skip cleanly when credentials are absent. Write tests are gated behind `HALAXY_ALLOW_WRITES`, only ever touch the pinned test patients, and clean up after themselves where the API allows (booked appointments are cancelled); endpoints that would create undeletable records in the practice are skipped by design — see `tests/Integration/UnexercisedWriteEndpointsTest.php`.

License
-------

[](#license)

MIT License. See [LICENSE](LICENSE) for details.

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/f2ea2e401c2f1f18cafc42d8fb98305ae3575db1fb25455af1c3156d0e79f90e?d=identicon)[wojt-janowski](/maintainers/wojt-janowski)

---

Top Contributors

[![wojt-janowski](https://avatars.githubusercontent.com/u/209190810?v=4)](https://github.com/wojt-janowski "wojt-janowski (3 commits)")

---

Tags

apilaravelsdkfhirhealthcareclinicallyhalaxy

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/clinically-laravel-halaxy/health.svg)

```
[![Health](https://phpackages.com/badges/clinically-laravel-halaxy/health.svg)](https://phpackages.com/packages/clinically-laravel-halaxy)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3355.4M352](/packages/psalm-plugin-laravel)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

78727.1M205](/packages/laravel-mcp)[propaganistas/laravel-disposable-email

Disposable email validator

6023.2M7](/packages/propaganistas-laravel-disposable-email)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k16.3M144](/packages/laravel-pulse)[defstudio/telegraph

A laravel facade to interact with Telegram Bots

817336.8k3](/packages/defstudio-telegraph)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9872.4M142](/packages/roots-acorn)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
