aftership/aftership-php-sdk
Official PHP SDK for the AfterShip tracking API. Manage shipments, couriers, and tracking statuses through a clean, framework-friendly client, with Laravel integration for configuration, service providers, and dependency injection.
Install and pin the version you have actually tested — 6.0.0 is a major bump whose only published note is "support 2024-01 version", so pin explicitly and don't float the constraint:
composer require aftership/aftership-php-sdk:^6.0
Because the release note is a single line, treat 6.0.0 as unverified until you've dumped one response. Upgrade path from 5.x: read the diff (vendor/aftership/aftership-php-sdk between tags) for changed method signatures, return envelopes, and exception classes before running it anywhere near production.
There is no Laravel service provider or facade in the package. Register it yourself in AppServiceProvider (or a small dedicated provider) and keep the key out of committed config:
// config/services.php
'aftership' => [
'key' => env('AFTERSHIP_API_KEY'),
'version' => '2024-01', // 6.0.0 targets this API version; see Gotchas
],
// app/Providers/AfterShipServiceProvider.php
use AfterShip\AfterShip;
public function register(): void
{
$this->app->singleton(AfterShip::class, fn () => new AfterShip(
config('services.aftership.key')
));
}
Read these first, in order: the package README's method list for couriers, trackings, and last_checkpoint; then the AfterShip API docs for the 2024-01 version (not the older docs you may already have bookmarked) covering POST /trackings and the webhook payload, because the SDK only covers request/response, not push updates.
First use case — create a tracking the moment an order ships, then enrich your UI from GET /trackings/{slug}/{number}:
use AfterShip\AfterShip;
$tracking = app(AfterShip::class)->trackings->create([
'tracking_number' => $order->tracking_number,
'slug' => $order->carrier_slug, // omit only if you trust auto-detect
'order_id' => (string) $order->id,
'title' => "Order #{$order->number}",
]);
$order->update(['aftership_id' => $tracking['id'] ?? null]);
Wrap the SDK behind your own interface. This is the single most valuable pattern here, and a major release that changes API versions is exactly the reason:
interface ShipmentTracker
{
public function createTracking(array $attributes): array;
public function find(string $slug, string $number): ?array;
}
The concrete class wraps AfterShip and translates SDK arrays into your own DTOs / value objects. When the SDK lags or breaks you swap one class for a Laravel Http client pointed at /2024-01 and nothing else changes.
Multi-tenant keys need a factory, not a singleton. If you're a SaaS where each tenant supplies their own AfterShip key, bind a factory that caches clients per key rather than overriding the singleton at runtime:
$this->app->bind(AfterShipFactory::class, fn ($app) => new AfterShipFactory());
// ->for($tenant->aftership_api_key)
Queue tracking creation. Every write should be a job with $tries, $backoff, and retryUntil, so a 429 or 5xx from AfterShip is retried with delay instead of failing a checkout or shipment action:
class CreateAfterShipTracking implements ShouldQueue
{
public int $tries = 5;
public array $backoff = [5, 15, 60, 300];
public function handle(ShipmentTracker $tracker): void { /* ... */ }
}
Hook creation to domain events, not controllers. A listener on ShipmentDispatched (or an observer on Order transitioning to shipped) keeps carrier logic out of HTTP layers. Guard with if ($order->tracking_number && $order->carrier_slug) and store the returned id for later reads.
Webhooks are the read path; polling is the fallback. AfterShip pushes updates; don't poll trackings->get() on page load. Set up a single unauthenticated route, verify the HMAC signature (AfterShip signs the raw body with your webhook secret), store the raw payload before processing, and dispatch an event:
Route::post('/webhooks/aftership', AfterShipWebhookController::class)
->withoutMiddleware(['csrf']);
$raw = $request->getContent();
$expected = hash_hmac('sha256', $raw, config('services.aftership.webhook_secret'));
abort_unless(hash_equals($expected, $request->header('aftership-hmac-sha256', '')), 401);
WebhookReceipt::create([
'event_id' => $request->header('aftership-event-id'),
'payload' => json_decode($raw, true),
]);
Add an idempotency layer. aftership-event-id and individual checkpoint IDs both work as dedupe keys. A unique index on the receipt table plus insertOrIgnore is enough. Process the receipt in a queued job — return 200 fast.
Map tag to your own enum. Values include Pending, InfoReceived, InTransit, OutForDelivery, AttemptFail, Delivered, AvailableForPickup, Exception, Expired. With the 2024-01 bump, re-check that list against current docs rather than trusting an enum you copied from a 2022 blog post. Don't let raw strings leak into blade templates or analytics.
Cache reads. Tracking detail rarely changes minute-to-minute; Cache::remember("aftership:{$slug}:{$number}", 300, ...) dramatically cuts your call volume and sidesteps rate-limit pain.
data/meta envelope, or exception class names changed. Diff the tags and run a smoke test against a sandbox key before upgrading from 5.x. Pin ^6.0 (or an exact version) and treat future releases as unverified until read./v4 or /v5 path is not the same contract as what the SDK talks to. If you fall back to a Http::withHeaders(['aftership-api-key' => $key]) client, target the matching version prefix and reconcile field names before switching traffic over.Http::fake() will not intercept this SDK. It uses its own transport, not Laravel's HTTP client, so your test suite must fake at your ShipmentTracker interface boundary — another reason to wrap it.data only vs. full body with meta). Dump one create() and one get() response in tinker and write a small normalizer.\Exception. There are distinct types for bad request, not found, unauthorized, rate limit, and internal error — re-confirm the namespace/hierarchy after upgrading to 6.0.0. Handle 429 separately and rethrow after $this->release($delay); immediate retries make it worse.slug + tracking_number pair fails with a 400-class error. On that specific failure, fall back to trackings->get($slug, $number) and treat it as success. This makes your job naturally retry-safe.couriers->detect with the number and let the user pick from the returned candidates — a wrong slug creates a tracking that will never update, and there is no "delete" you should rely on for cleanup.AfterShip instance holds the API key, and SDK error messages can echo request context. Log meta.code/meta.message and your own correlation ID.checkpoint_time (or checkpoint_id) before displaying, and ->utc()/->tz() them for the user's locale.How can I help you explore Laravel packages today?