daikazu/asi-smartlink

A typed PHP client for the ASI Smartlink REST API, built on Saloon, with fluent query builders, immutable DTOs, typed exceptions, and optional Laravel integration.

Maintainers

Package info

github.com/daikazu/asi-smartlink

pkg:composer/daikazu/asi-smartlink

Fund package maintenance!

Daikazu

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-06-05 18:31 UTC

This package is auto-updated.

Last update: 2026-06-05 18:42:09 UTC


README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

A typed PHP client for the ASI Smartlink REST API, built on Saloon. Search products, suppliers, and decorators; pull product detail, charges, and price matrices; resolve media URLs; and drive auto-complete and industry news — all through fluent query builders, immutable DTOs, and typed exceptions. Laravel integration (service provider, facade, publishable config) is included but optional; the client runs equally well in plain PHP.

Warning

Work in progress — pre-release software. This package is under active development and has not yet reached a stable 1.0 release. The public API, DTO shapes, and method signatures may change without notice between 0.x releases. Pin an exact version and review the changelog before upgrading. Not yet recommended for production use.

Installation

You can install the package via composer:

composer require daikazu/asi-smartlink

You can publish the config file with:

php artisan vendor:publish --tag="asi-smartlink-config"

Environment

Add your ASI-issued credentials to .env:

ASI_SMARTLINK_API_URL=https://api.asicentral.com
ASI_SMARTLINK_CLIENT_ID=your-client-id
ASI_SMARTLINK_SECRET=your-client-secret
# optional
ASI_SMARTLINK_TIMEOUT=30

Authentication uses ASI's AsiMemberAuth scheme — a single Authorization header built from the client ID and secret. There is no OAuth/token exchange.

Usage

use Daikazu\AsiSmartlink\Enums\ProductSort;
use Daikazu\AsiSmartlink\Facades\AsiSmartlink;

$results = AsiSmartlink::products()->search(
    query: 'mugs',
    page: 1,
    resultsPerPage: 25,
    sort: ProductSort::PriceLowToHigh,
    dimensionLists: ['supplier', 'category'],
);

$results->resultsTotal;          // 20552
$results->hasNextPage();         // true

foreach ($results->products as $product) {
    $product->name;              // "Purple Plush Santa Hat"
    $product->number;            // "HAT112"
    $product->supplier?->name;   // "Brighter Promotions Inc"
    $product->prices[0]?->price; // 3.68
}

The same is available via the container, e.g. app(\Daikazu\AsiSmartlink\AsiSmartlink::class) or by injecting Daikazu\AsiSmartlink\Http\SmartLinkConnector.

Using it without Laravel

Laravel is optional. The package only requires saloonphp/saloon at runtime — the service provider, facade, and config helpers are conveniences that activate when Laravel is present. Outside Laravel (plain PHP, Symfony, a CLI script, …) just construct the connector directly:

use Daikazu\AsiSmartlink\Http\SmartLinkConnector;
use Daikazu\AsiSmartlink\Support\ProductQuery;

$smartlink = new SmartLinkConnector(
    baseUrl: 'https://api.asicentral.com',
    clientId: 123456,
    clientSecret: getenv('ASI_SMARTLINK_SECRET'),
);

$results = $smartlink->products()->search(
    ProductQuery::make('pen')->hasImage()->priceBetween(1, 5)
);

The resources (->products(), ->suppliers(), ->decorators(), ->lists(), ->media()), DTOs, query builders, and typed exceptions all work identically — none of them touch the framework. Saloon's own MockClient ($connector->withMockClient(...)) handles testing without the Laravel Saloon::fake() facade.

Available resources

use Daikazu\AsiSmartlink\Facades\AsiSmartlink;
use Daikazu\AsiSmartlink\Enums\AutoCompleteDimension;
use Daikazu\AsiSmartlink\Enums\MediaSize;

// Products
AsiSmartlink::products()->search('mugs');
AsiSmartlink::products()->get(4815380);                 // full detail
AsiSmartlink::products()->getByCpn('12345');            // CPN lookup
AsiSmartlink::products()->configure(4815380, configId: 1);
AsiSmartlink::products()->matrix(4694711);              // apparel price grid
AsiSmartlink::products()->charges(4815380);             // list<ProductCharge>
AsiSmartlink::products()->lookupCharges();              // master charge list
AsiSmartlink::products()->deletedSince('11/02/2016');   // list<int>

// Suppliers & decorators
AsiSmartlink::suppliers()->search('promotions');
AsiSmartlink::suppliers()->get(875);
AsiSmartlink::decorators()->search('promotions');
AsiSmartlink::decorators()->get(6859235);

// Lists (no auth required)
AsiSmartlink::lists()->autoComplete(AutoCompleteDimension::Category, 'shirts'); // list<string>
AsiSmartlink::lists()->news();                          // list<NewsArticle>
AsiSmartlink::lists()->newsArticle(46318);

// Media (no auth required)
AsiSmartlink::media()->url(20542352, MediaSize::Normal); // build URL for <img>
AsiSmartlink::media()->get(5072512, MediaSize::Large);   // fetch the image bytes

Authentication: most endpoints send the AsiMemberAuth credentials automatically. The auto-complete, industry-news, and media endpoints require no authentication, so the package deliberately omits the credentials on those requests.

Note on detail responses: ProductDetail, SupplierDetail, and DecoratorDetail type the documented identity/contact fields and expose the complete decoded payload via a raw array (and a get('Key') helper). The SmartLink PDF's "Raw Response" samples are embedded images, so the deeper nested structures carry TODO(confirm) notes pending verification against a live response — see the DTO docblocks.

Filtering product search

The product search q parameter supports dimension and boolean filters. Build them with ProductQuery instead of hand-writing the string — products()->search() accepts either a plain keyword string or a ProductQuery:

use Daikazu\AsiSmartlink\Facades\AsiSmartlink;
use Daikazu\AsiSmartlink\Support\ProductQuery;

$query = ProductQuery::make('pen')
    ->preferred(1, 2, 3)        // preferred supplier ranks 1-5 (or ->preferredAll() for *)
    ->category('Pens')
    ->priceBetween(1.00, 3.00)  // price:[1 to 3]
    ->hasImage()                // boolean filters: has_image:1
    ->isNew();

AsiSmartlink::products()->search($query, resultsPerPage: 25);
// q => "pen preferred:1,2,3 category:Pens price:[1 to 3] has_image:1 is_new:1"

Covers all tier-1 dimensions (category, color, size, material, supplier, asi, priceBetween/costBetween/profitBetween, quantity, productionTime, supplierCountry, preferred), tier-2 dimensions (shape, theme, tradeName, supplierState, imprintMethod, lineName, supplierRating), the boolean filters (hasImage, hasPrice, hasRushService, isNew, isSoldBlank, isMadeInUsa, isConfirmedProduct, …), and a raw('filter:value') escape hatch. Per the ASI docs, combine a tier-2 filter with at least one tier-1 selection, and preferred:* slows the response.

Supplier search has its own builder, SupplierQuery, accepted by suppliers()->search():

use Daikazu\AsiSmartlink\Support\SupplierQuery;

// Filter-only search — suppliers in Philadelphia, PA, rated 4+
AsiSmartlink::suppliers()->search(
    SupplierQuery::make()
        ->state('PA')              // postal abbreviation
        ->city('Philadelphia')
        ->supplierRating(4)        // minimum rating; emits rating:4
);
// q => "state:PA city:Philadelphia rating:4"

The keyword is AND-ed with every filter. It matches against the supplier name, so pairing a specific keyword with a narrow location easily yields zero results (e.g. make('promotions')->city('Philadelphia') finds no supplier named "promotions" in Philadelphia). Use the keyword for name searches, or omit it and filter by location. supplierRating(n) matches suppliers rated n or higher.

Supports asi, zip, phone, city, state, email, supplierRating, canadianFriendly, canadian, and a raw('filter:value') escape hatch.

Decorator search has the equivalent DecoratorQuery (a smaller filter set: asi, zip, phone, city, state, email), accepted by decorators()->search():

use Daikazu\AsiSmartlink\Support\DecoratorQuery;

AsiSmartlink::decorators()->search(
    DecoratorQuery::make('embroidery')->state('FL')->city('Miami')
);
// q => "embroidery state:FL city:Miami"

state: expects the postal abbreviation (FL, PA), even though the address in responses contains the full state name.

Autocomplete → filter

The dimension filters (category, color, supplier, theme, …) match exact values, so use the auto-complete endpoint to discover valid values as the user types, then feed the chosen value straight into the query builder. Auto-complete needs no auth and isn't rate-limited, so it's safe to call on each keystroke.

use Daikazu\AsiSmartlink\Enums\AutoCompleteDimension;
use Daikazu\AsiSmartlink\Facades\AsiSmartlink;
use Daikazu\AsiSmartlink\Support\ProductQuery;

// 1. user types "bl" in the colour field — suggest matching values
$suggestions = AsiSmartlink::lists()->autoComplete(AutoCompleteDimension::Color, 'bl');
// ["Black Shades", "Blue Shades", "Bright Blue", ...]

// 2. user picks one — feed the exact value into the search filter
$results = AsiSmartlink::products()->search(
    ProductQuery::make('shirt')->color('Blue Shades')
);

The AutoCompleteDimension cases line up with the builder methods (Colorcolor(), Categorycategory(), Suppliersupplier(), Themetheme(), and so on). Supplier suggestions come back as "Hit Promotional Products (asi/61125)", so you can show the name and parse the ASI number for an asi: filter.

Error handling

Failed responses (4xx/5xx) throw a typed exception. ASI returns errors as {"Error": "message"}; that message is surfaced on the exception. All exceptions extend SmartLinkRequestException, so you can catch one or many:

use Daikazu\AsiSmartlink\Exceptions\AuthenticationException;
use Daikazu\AsiSmartlink\Exceptions\ResourceNotFoundException;
use Daikazu\AsiSmartlink\Exceptions\RateLimitExceededException;
use Daikazu\AsiSmartlink\Exceptions\SmartLinkRequestException;

try {
    $product = AsiSmartlink::products()->get(4815380);
} catch (ResourceNotFoundException $e) {
    // 404 — {"Error":"Product not found"}
} catch (AuthenticationException $e) {
    // 401/403 — bad or missing client_id / client_secret
} catch (RateLimitExceededException $e) {
    // 429 — hourly limit (5,000) exceeded
    $e->limit();      // 5000
    $e->remaining();  // 0
    $e->reset();      // X-Rate-Limit-Reset
} catch (SmartLinkRequestException $e) {
    // any other API error (e.g. 5xx)
    $e->getStatus();    // HTTP status code
    $e->error();        // ASI "Error" message (null if the body wasn't JSON)
    $e->getResponse();  // the underlying Saloon response
}
Status Exception
401 / 403 AuthenticationException
404 ResourceNotFoundException
429 RateLimitExceededException
other 4xx/5xx SmartLinkRequestException

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.