PHPackages                             starmile/partner-sdk - 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. starmile/partner-sdk

ActiveLibrary[API Development](/categories/api)

starmile/partner-sdk
====================

Official PHP SDK for the Starmile Partner API — cross-border logistics integration (catalogue, orders, status pool, inbound events).

v6.4.0(2d ago)032↑1118.8%MITPHP &gt;=7.1

Since Jul 5Compare

[ Source](https://github.com/starmile-io/partner-sdk-php)[ Packagist](https://packagist.org/packages/starmile/partner-sdk)[ Docs](https://api.starmile.io)[ RSS](/packages/starmile-partner-sdk/feed)WikiDiscussions Synced today

READMEChangelogDependencies (1)Versions (6)Used By (0)

Starmile Partner SDK (PHP)
==========================

[](#starmile-partner-sdk-php)

Official PHP SDK for the **Starmile Partner API** — the partner-facing integration surface of the Starmile cross-border logistics platform. It wraps every Partner API capability behind a small, typed, dependency-free client: catalogue lookup, order intake and management, the pull-based status pool, and inbound lifecycle events.

- **No runtime dependencies** beyond `ext-curl` and `ext-json` — installs cleanly on legacy stacks.
- **Wide PHP support: 7.1 → 8.4.**
- **Automatic OAuth2** — tokens are fetched, cached, and refreshed for you.
- **Typed exceptions** mapped from HTTP status codes.
- **Fluent builders** for orders, parcels, products, and events, with local validation of event types and fields before anything hits the network.

> This package is a thin, faithful client over the HTTP API. The HTTP API remains the canonical contract; the public reference lives at [api.starmile.io](https://api.starmile.io).

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

[](#installation)

```
composer require starmile/partner-sdk
```

Requires PHP **7.1+** with the `curl` and `json` extensions.

Authentication
--------------

[](#authentication)

The Partner API uses **OAuth2 client credentials**. Your organization's back office issues you a `client_id` and `client_secret` (a Partner API credential). The SDK exchanges them for a short-lived bearer token at `POST /oauth/token` and attaches it to every call — you never touch the token endpoint directly.

```
use Starmile\PartnerSdk\Client;

$starmile = Client::create(
    getenv('STARMILE_CLIENT_ID'),
    getenv('STARMILE_CLIENT_SECRET')
);
```

Keep the `client_secret` server-side. Read it (and the base URL) from the environment — never hardcode credentials or per-environment hosts.

### Configuration options

[](#configuration-options)

`Client::create($clientId, $clientSecret, $options)` accepts:

OptionDefaultDescription`base_url``https://api.starmile.io`API host. Point at your sandbox/staging outside production.`scopes`all grantedSubset of the credential's scopes to request on the token.`http_client`bundled cURL clientAny `HttpClientInterface` (Guzzle/Symfony/PSR-18 adapter, a mock).`token_storage`in-memoryA `TokenStorageInterface` to share one token across processes.`verify_tls``true`TLS verification (keep on in production).`connect_timeout``10`Connection timeout (seconds).`timeout``30`Total request timeout (seconds).`user_agent``starmile-partner-sdk-php/x`Override the `User-Agent`.`max_attempts``3`Total attempts for **safe (GET)** calls on transient failure. `1` disables auto-retry.`retry_base_delay_ms``200`Base backoff between retries (exponential + jitter).`retry_max_delay_ms``5000`Cap on the computed backoff.Capabilities
------------

[](#capabilities)

The Partner API surface is reached through four resource groups, each gated by the scopes on your credential.

ResourceScope(s)Endpoints`$starmile->catalogue()``catalogue:read``GET /api/v1/services`, `GET /api/v1/rates``$starmile->orders()``orders:create`, `orders:update`, `orders:cancel``POST /api/v1/orders`, `PATCH /api/v1/orders/{order}/parcels/{parcel}`, `POST /api/v1/orders/{order}/cancel``$starmile->statusPool()``status:read``GET /api/v1/partner/changes``$starmile->events()``events:transport`, `events:pudo`, `events:customs`, `leg:handoff``POST /api/v1/partner/events`### Catalogue

[](#catalogue)

```
$services = $starmile->catalogue()->services(); // valid service_id values to order against
$rates    = $starmile->catalogue()->rates();    // the rates bound to your partner
```

Each rate's `service_id` is a **Service's own `id`** (the same value you send on order creation), so a rate maps to exactly one Service — correlate a rate to a service by matching `$rate['service_id']` to `$service['id']`.

### Orders

[](#orders)

Build an order with the fluent builders (or pass a raw array matching the API body). The corridor and delivery type come from the **Service**; you do not send a rate — Starmile resolves the billing rate automatically.

```
use Starmile\PartnerSdk\Builder\OrderBuilder;
use Starmile\PartnerSdk\Builder\ParcelBuilder;
use Starmile\PartnerSdk\Builder\ProductBuilder;

$order = OrderBuilder::make($serviceId, 'ORD-1001')   // service_id + your order_id
    ->recipient('Jane Doe', '+994500000000', 'jane@example.com', '5AB12C3')  // 4th arg = gov_id (AZ FIN / passport)
    ->deliverToPudo(42)                                // or ->deliverHome('1', '2') / ->deliverToLocker($lockerId)
    ->shippingCost(9.90)
    ->addParcel(
        ParcelBuilder::make('ITEM-1')                // your per-item reference (echoed back as partner_tracking)
            ->merchantTracking('BARCODE-1')            // the physical sticker code (merchant_tracking)
            ->weightGrams(1200)
            ->addProduct(
                ProductBuilder::make('Running shoes')
                    ->hsCode('640299')
                    ->declaredValue(59.99, 'USD')
                    ->quantity(1)
            )
    );

$created = $starmile->orders()->create($order);
echo $created['order_id'];              // STM… (our order id)
echo $created['region_status'];         // mapped | pending_mapping | not_applicable
echo $created['items'][0]['parcel_id']; // STM… (our parcel id for your item_id)
```

For a **Home Delivery** service the destination region is resolved from your own `(parent_region, region)` reference, map-only per partner. If it is not mapped yet the order is **still accepted** and comes back with `region_status``pending_mapping` — an operator maps your region in Starmile and the waiting order resolves automatically, so you do **not** resend it. `mapped` means the region was resolved; `not_applicable` means the service has no home region (PUDO / locker / clearance). Compare against `Starmile\PartnerSdk\Enum\RegionStatus`.

Orders and parcels are addressed by **your own references** afterwards — the `order_id` you sent, and a parcel's `item_id` (which the parcel carries back as `partner_tracking` in responses):

```
// Update a shipment that has not been received yet (partial; `products` replaces the list).
$starmile->orders()->updateParcel('ORD-1001', 'ITEM-1', [
    'weight_grams' => 1500,
    'merchant_tracking' => 'BARCODE-1B',
]);

// Cancel an order while it is still pre-custody (409 once in custody).
$starmile->orders()->cancel('ORD-1001', 'customer changed mind');
```

### Parcel labels (PDF)

[](#parcel-labels-pdf)

Download a SINGLE parcel's label as a PDF, rendered from your organization's default parcel label template. Address the parcel by its `merchant_tracking` (sticker code) or its `parcel_id` (our parcel id, returned as items\[\].parcel\_id on create). The method returns the raw PDF bytes. Scope: `labels:read`.

```
// By merchant_tracking (sticker code).
file_put_contents('label.pdf', $starmile->orders()->label('BARCODE-1'));

// By parcel_id (our parcel id, from items[].parcel_id on create).
$pdf = $starmile->orders()->labelByParcelId('STM0000000121');
```

### Status pool (replaces webhooks)

[](#status-pool-replaces-webhooks)

Poll the pool with the cursor you last processed; you receive every change after it, in order. Persist the returned cursor to resume next time.

```
// One page at a time:
$page = $starmile->statusPool()->changes($since = 0, $limit = 100);
foreach ($page->changes() as $change) {
    // $change['cursor'], ['tracking_number'], ['external_parent_id'], ['external_id'], ['country'], ['status'], ['previous_status'], ['occurred_at']
}
$next = $page->nextCursor();
$more = $page->hasMore();

// Or drain everything, auto-paging:
foreach ($starmile->statusPool()->each($since = 0) as $change) {
    $since = $change['cursor']; // persist this
}
```

`external_parent_id` is your own reference for the **order**. `external_id` is your reference for a single **parcel** (the `item_id` you sent on create) and is present only on parcel-scoped changes — e.g. a parcel received at the hub — so you can act on that exact parcel; order-level changes leave `external_id` null. `country` is the ISO-2 country the change occurred in (the hub's country), so you can tell an origin event (e.g. `CN`) apart from a destination one (e.g. `AZ`).

### Inbound events

[](#inbound-events)

Report a lifecycle event for a package you handle as a carrier (`shipment.*`), PUDO point (`parcel.*`), customs broker (`customs.*`), or foreign organization on the next leg (`leg.*`). The `EventBuilder` validates the type and its `data` fields locally — an unknown type or field throws before a request is made.

```
use Starmile\PartnerSdk\Enum\EventType;

$outcome = $starmile->events()->reportEvent(
    EventType::SHIPMENT_OUT_FOR_DELIVERY,
    $trackingNumber,
    'evt-0001',                              // your idempotency key
    ['driver' => 'Driver A', 'eta' => '2026-06-28T09:00:00Z']
);
// $outcome['result'], $outcome['order_status']
```

Each accepted event advances the package. An event that is not a legal next step for the package's current status is rejected with a `422` carrying an `error` and a `hint` (see error handling below).

The recognised event types, their scope, and their `data` fields are exposed on the `EventType` enum:

```
EventType::all();                                     // every recognised type
EventType::scopeFor(EventType::CUSTOMS_HELD);         // 'events:customs'
EventType::dataFieldsFor(EventType::SHIPMENT_DELIVERED); // ['note','recipient_name','signed_by','proof_of_delivery']
```

Error handling
--------------

[](#error-handling)

Non-2xx responses raise a typed exception; all extend `StarmileException`.

ExceptionHTTPMeaning`AuthenticationException`401Missing/expired/invalid token or bad credentials.`AuthorizationException`403Credential lacks the scope, is revoked, or API off.`NotFoundException`404Unknown resource (or another partner's).`ConflictException`409No longer changeable (shipment received / in custody).`ValidationException`422Validation failed; field errors + event `hint`.`RateLimitException`429Too many requests; carries `getRetryAfter()`.`ApiException`otherAny other non-2xx; base for the above.`TransportException`—Network failure (no response).```
use Starmile\PartnerSdk\Exception\ValidationException;
use Starmile\PartnerSdk\Exception\RateLimitException;
use Starmile\PartnerSdk\Exception\StarmileException;

try {
    $starmile->orders()->create($order);
} catch (ValidationException $e) {
    $e->errors();      // ['service_id' => ['The service id field is required.'], ...]
    $e->allMessages(); // flat list
    $e->getHint();     // event rejections include a "how to fix" hint
} catch (RateLimitException $e) {
    sleep($e->getRetryAfter() ?: 1);
} catch (StarmileException $e) {
    // any other failure
    $e->getMessage();
}
```

Retries &amp; resilience
------------------------

[](#retries--resilience)

By default the SDK retries **safe (GET)** requests on transient failures — network errors, `429`, and `5xx` — with exponential backoff + jitter, honoring a `Retry-After` header. Non-idempotent writes (`POST /orders`, `POST /partner/events`) are **never** retried automatically, so a flaky response can't create a duplicate order. Tune or disable this with `max_attempts` (see options above).

When you *do* want a write retried, opt in per call with `retry()` — mirroring Laravel's HTTP client. It returns a one-off client; the original is unchanged:

```
// Retry this create up to 3 times (writes included, because you asked):
$starmile->retry(3, 200)->orders()->create($order);

// Custom decision — also retry a specific conflict:
use Starmile\PartnerSdk\Exception\RateLimitException;

$starmile
    ->retry(4, 200, fn ($e) => $e instanceof RateLimitException || $e->getStatusCode() === 409)
    ->events()->report($event);
```

When a failure can't be decoded as JSON (e.g. a gateway's HTML `502`), the raw body is preserved on the exception via `getRawBody()`:

```
catch (\Starmile\PartnerSdk\Exception\ApiException $e) {
    $e->getResponseBody(); // [] when the body wasn't JSON
    $e->getRawBody();      // the original "...502 Bad Gateway..." string
}
```

Constants
---------

[](#constants)

The SDK ships the server vocabularies verbatim so you compare against names, not magic strings:

- `Starmile\PartnerSdk\Enum\Scope` — every OAuth2 scope.
- `Starmile\PartnerSdk\Enum\EventType` — every inbound event type (+ scope/field maps).
- `Starmile\PartnerSdk\Enum\OrderStatus` — every status seen in the pool.
- `Starmile\PartnerSdk\Enum\PackageType` — `fragile` / `breakable` / `liquid`.
- `Starmile\PartnerSdk\Enum\DeliveryMethod` — `home` / `pudo` / `locker`.

Custom HTTP transport
---------------------

[](#custom-http-transport)

Replace the default cURL transport with anything implementing `HttpClientInterface` (e.g. to reuse a configured Guzzle client, add retries, or mock in tests):

```
use Starmile\PartnerSdk\Client;
use Starmile\PartnerSdk\Http\HttpClientInterface;
use Starmile\PartnerSdk\Http\RawResponse;

final class GuzzleTransport implements HttpClientInterface
{
    public function send($method, $url, array $headers = [], $body = null)
    {
        // ... call Guzzle, then:
        return new RawResponse($statusCode, $responseHeaders, $responseBody);
    }
}

$starmile = Client::create($id, $secret, ['http_client' => new GuzzleTransport()]);
```

Token sharing across processes
------------------------------

[](#token-sharing-across-processes)

By default the token lives in process memory. Implement `TokenStorageInterface`(backed by APCu, Redis, a PSR-16 cache, or a file) and pass it as `token_storage`to reuse one token across requests/workers and avoid re-hitting `/oauth/token`.

Testing
-------

[](#testing)

```
composer install
composer test
```

The suite drives the client through a scripted in-memory transport — no network.

Versioning &amp; support
------------------------

[](#versioning--support)

Semantic Versioning. New Partner API capabilities are added here in lockstep with the server; see the [CHANGELOG](CHANGELOG.md).

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance100

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity32

Early-stage or recently created project

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

4

Last Release

2d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/54883542?v=4)[Samir Mammadhasanov](/maintainers/samirmhsnv)[@samirmhsnv](https://github.com/samirmhsnv)

---

Tags

sdkshippinglogisticscross-borderPartner-APIstarmile

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/starmile-partner-sdk/health.svg)

```
[![Health](https://phpackages.com/badges/starmile-partner-sdk/health.svg)](https://phpackages.com/packages/starmile-partner-sdk)
```

###  Alternatives

[octw/aramex

A Library to integrate with Aramex APIs

2927.7k](/packages/octw-aramex)

PHPackages © 2026

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