PHPackages                             rebelpl/bc-api2-client - 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. rebelpl/bc-api2-client

ActiveLibrary

rebelpl/bc-api2-client
======================

Business Central API interface

0228↓100%PHP

Since Oct 23Pushed 4mo ago1 watchersCompare

[ Source](https://github.com/rebelpl/bc-api2-client)[ Packagist](https://packagist.org/packages/rebelpl/bc-api2-client)[ RSS](/packages/rebelpl-bc-api2-client/feed)WikiDiscussions main Synced today

READMEChangelogDependenciesVersions (2)Used By (0)

Business Central API2 for PHP
=============================

[](#business-central-api2-for-php)

This library includes client to use [Business Central API (v2.0)](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/) in PHP. Base models for the standard Microsoft API v2 are available here: [rebelpl/bc-api2-common](https://github.com/rebelpl/bc-api2-common).

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

[](#installation)

To install, use composer:

```
composer require rebelpl/bc-api2-client

```

To use standard resources provided by Microsoft:

```
composer require rebelpl/bc-api2-common

```

Usage
-----

[](#usage)

### Setup

[](#setup)

To use the client, you need OAuth authentication flow to be set up for your app: .

### Create client

[](#create-client)

To create a client, you need a valid Access Token. You can use an OAuth library to obtain it, then:

```
$client = new Rebel\BCApi2\Client(
    accessToken: $accessToken,
    environment: 'sandbox',
    companyId: '123456',
);
```

or use `Client\Factory` helper (requires [`rebelpl/oauth2-businesscentral`](https://github.com/rebelpl/oauth2-businesscentral)or any other implementation of `League\OAuth2\Client\Provider\AbstractProvider`):

```
// service-to-service
$client = Rebel\BCApi2\Client\Factory::useClientCredentials(
    new Rebel\OAuth2\Client\Provider\BusinessCentral([
        'tenantId' => 'mydomain.com',
        'clientId' => 'xxxxx-yyyy-zzzz-xxxx-yyyyyyyyyyyy',
        'clientSecret' => '*************************',
    ]),
    environment: 'sandbox',
    companyId: '123456');
```

```
// login-as
$client = Rebel\BCApi2\Client\Factory::useAuthorizationCode(
    new Rebel\OAuth2\Client\Provider\BusinessCentral([
        'tenantId' => 'mydomain.com',
        'clientId' => 'xxxxx-yyyy-zzzz-xxxx-yyyyyyyyyyyy',
        'clientSecret' => '*************************',
        'redirectUri' => 'https://localhost',
    ]),
    environment: 'sandbox',
    companyId: '123456',
    tokenFilename: 'tmp/token.json');
```

### Get Companies

[](#get-companies)

```
foreach ($client->getCompanies() as $company) {
    echo " - {$company->name}:\t{$company->id}\n";
}
```

### Get Resources

[](#get-resources)

```
$response = $client->get('companies(123456)/items?$top=3');
$data = json_decode($response->getBody(), true);
foreach ($data['value'] as $item) {
    echo " - {$item['number']}:\t{$item['displayName']}\n";
}
```

### Use Request helper

[](#use-request-helper)

```
$etag = urldecode($item['@odata.etag']);
$request = new Rebel\BCApi2\Request('PATCH', 'companies(123456)/items(32d80403)',
    body: json_encode([
        'displayName' => 'Updated Item Name',
        'unitPrice' => 99.95,
     ]), etag: $etag);
$response = $client->call($request);
```

### Use Repository / Entity helpers

[](#use-repository--entity-helpers)

```
# find a single customer
$repository = new Rebel\BCApi2\Entity\Repository($client, entitySetName: 'customers');
if ($customer = $repository->findOneBy([ 'number' => 'CU-TEST' ])) {
    echo " - {$customer->get('number')}:\t{$customer->get('displayName')} @ {$customer->get('country')} ({$customer->get('id')})\n";
}

# find sales orders based on given criteria
$repository = new Rebel\BCApi2\Entity\Repository($client, entitySetName: 'salesOrders');
$repository->setExpandedByDefault([ 'salesOrderLines' ]);
$results = $repository->findBy([
    'customerNumber' => [ 'CU-TEST', 'CU-0123' ]
    'customerPriceGroup' => 'GOLD'
], 'orderDate DESC', size: 5);
foreach ($results as $salesOrder) {

    # use rebelpl/bc-api2-common or generate your own models for easier access to properties
    echo " - {$salesOrder->get('number')}:\t{$salesOrder->get('totalAmountIncludingTax')} {$salesOrder->get('currencyCode')}\n";
    foreach ($salesOrder->get('salesOrderLines', 'collection') as $line) {
        echo " --- {$line->get('sequence')}:\t{$line->get('lineObjectNumber')} x {$line->get('quantity')}";
    }
}

# create new salesOrder
$salesOrder = new Rebel\BCApi2\Entity([
    'customerNumber' => 'CU-0123',
    'externalDocumentNumber' => 'TEST/123',
    'salesOrderLines' => new Rebel\BCApi2\Entity\Collection([

        new Rebel\BCApi2\Entity([
            "sequence" => 10000,
            "lineType" => "Item",
            "lineObjectNumber" => "1900-A",
            "quantity" => 5
        ],

        new Rebel\BCApi2\Entity([
            "sequence" => 20000,
            "lineType" => "Item",
            "lineObjectNumber" => "1928-S",
            "quantity" => 20
        ],
    ]),
]);

$repository->create($salesOrder);
echo " - {$salesOrder->get('number')}:\t{$salesOrder->get('totalAmountIncludingTax')} {$salesOrder->get('currencyCode')}\n";

# filter sales orders and sales lines at the same time
$results = $repository->findBy([ 'sellToCountry' => ['PL', 'UK'] ], top: 10, expanded: [
    'salesOrderLines' => [ 'lineType' => 'Item', Rebel\BCApi2\Request\Expression::greaterThan('quantity', 5) ],
]);
echo count($results) . " sales orders found, only lines with quantity > 5 included.\n";
```

### Working with binary data streams (BLOB)

[](#working-with-binary-data-streams-blob)

If the field in BC is stored as BLOB, it's accessible through API as Edm.Stream type. In order to access (read or write) its contents, you need to make additional call to the URL listed as @odata.mediaReadLink / @odata.mediaEditLink.

```
$repository = new Rebel\BCApi2\Entity\Repository($client, 'items');
$item = $repository->findOneBy([ 'number' => '100000' ], [ 'picture' ]);
$picture = $item->get('picture');

# download to a file
if ($picture->get('contentType')) {
    file_put_contents('path/to/file.png', $picture->get('pictureContent')->downloadWith($client));
}

# upload from a file
$picture->get('pictureContent')->uploadWith($client, file_get_contents('path/to/file.png'), $picture->getETag());

# download a stream without expanding the record
$repository = new Rebel\BCApi2\Entity\Repository($client, 'salesInvoices');
$invoices = $repository->findBy([ 'isClosed' => false, 'dueDate le 2025-09-19' ]);
foreach ($invoices as $invoice) {
    file_put_contents('path/to/' . $invoice->get('number') . '.pdf', $invoice->fetchAsStream('pdfDocument/pdfDocumentContent'));
}
```

### Deep update with expanded properties

[](#deep-update-with-expanded-properties)

Business Central does not support deep update and mixed insert/update operations. The Entity\\Repository class provides a custom save() method that handles this limitation by using batchUpdate() to create / update the nested properties.

```
// Get a sales order by ID
$repository = new Rebel\BCApi2\Entity\Repository($client, 'salesOrders');
$salesOrder = $repository->get('abc-123', [ 'salesOrderLines' ]);

// Update properties of the sales order
$salesOrder->set('externalDocumentNumber', 'TEST');
$salesLines = $salesOrder->get('salesOrderLines');

// Update existing line
$salesLines[0]->set('quantity', 10);

// Add new line
$salesLines[] = new Rebel\BCApi2\Entity([
    'itemId' => '12345',
    'quantity' => 5
]);

// Save all changes in one operation
$repository->save($salesOrder);
```

### Call bound action

[](#call-bound-action)

```
// Create a SalesOrder repository
$repository = new Rebel\BCApi2\Entity\SalesOrder\Repository($client);
$salesOrder = $repository->get('abc-123');
$repository->callBoundAction('Microsoft.NAV.shipAndInvoice', $salesOrder);
```

Download metadata for your API
------------------------------

[](#download-metadata-for-your-api)

```
curl -X GET "https://api.businesscentral.dynamics.com/v2.0//api////$metadata" \
  -H "Authorization: Bearer " \
  -H "Accept: application/xml" \
  -o files/metadata.xml
```

### Generate Entity models for your API

[](#generate-entity-models-for-your-api)

```
# fetch Metadata from BC...
$metadata = new Rebel\BCApi2\Client(
    accessToken: $token->getToken(),
    environment: 'sandbox',
    apiRoute: '/mycompany/myapi/v1.5'
)->getMetadata();

# ... or from the local file
$metadata = Rebel\BCApi2\Metadata\Factory::fromString(file_get_contents('files/metadata.xml'));

# then generate the files
$generator = new Rebel\BCApi2\Entity\Generator($metadata, namespacePrefix: 'App\\Models\\');
$generator->saveAllFilesTo('app/Models', overwrite: true);
```

Tests
=====

[](#tests)

```
./vendor/bin/phpunit

```

Known Limitations
=================

[](#known-limitations)

The client currently does not support complex primary keys (TBD).

Read-only properties on otherwise editable entities (like customerName on salesOrder) are not hinted as read-only in metadata, so the Generator still generates a property setter hook, even if it's useless.

###  Health Score

22

—

LowBetter than 25% of packages

Maintenance51

Moderate activity, may be stable

Popularity13

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity14

Early-stage or recently created project

 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.

### Community

Maintainers

![](https://www.gravatar.com/avatar/f07dd01ff64eaf2de9ffc803220c3ab4cdfcfa049991af3e028da34c03430714?d=identicon)[nataniel](/maintainers/nataniel)

---

Top Contributors

[![nataniel](https://avatars.githubusercontent.com/u/411846?v=4)](https://github.com/nataniel "nataniel (49 commits)")

### Embed Badge

![Health badge](/badges/rebelpl-bc-api2-client/health.svg)

```
[![Health](https://phpackages.com/badges/rebelpl-bc-api2-client/health.svg)](https://phpackages.com/packages/rebelpl-bc-api2-client)
```

PHPackages © 2026

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