PHPackages                             laratusk/spreedly - 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. [Payment Processing](/categories/payments)
4. /
5. laratusk/spreedly

ActiveLibrary[Payment Processing](/categories/payments)

laratusk/spreedly
=================

PHP SDK for the Spreedly payment orchestration API with Laravel support

v2.0.0(1w ago)221MITPHPPHP ^8.2CI passing

Since Feb 26Pushed 1w agoCompare

[ Source](https://github.com/laratusk/spreedly-php)[ Packagist](https://packagist.org/packages/laratusk/spreedly)[ Docs](https://github.com/laratusk/spreedly)[ RSS](/packages/laratusk-spreedly/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (6)Dependencies (36)Versions (9)Used By (0)

Spreedly PHP SDK
================

[](#spreedly-php-sdk)

A production-ready PHP SDK for the [Spreedly payment orchestration API](https://spreedly.com), following the Stripe PHP SDK architecture. Works as a standalone PHP library or as a Laravel package.

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

[](#requirements)

- PHP ^8.2
- Laravel ^10.0 || ^11.0 || ^12.0 || ^13.0 (optional)

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

[](#installation)

```
composer require laratusk/spreedly
```

Standalone PHP Usage
--------------------

[](#standalone-php-usage)

```
$spreedly = new \Laratusk\Spreedly\SpreedlyClient(
    environmentKey: 'your_environment_key',
    accessSecret: 'your_access_secret',
);
```

### Configuration Options

[](#configuration-options)

```
$spreedly = new \Laratusk\Spreedly\SpreedlyClient(
    environmentKey: 'your_environment_key',
    accessSecret: 'your_access_secret',
    options: [
        'base_url'        => 'https://core.spreedly.com/v1/',
        'timeout'         => 30,
        'connect_timeout' => 10,
        'retries'         => 3,
    ],
);
```

Laravel Usage
-------------

[](#laravel-usage)

Publish the config file:

```
php artisan vendor:publish --provider="Laratusk\Spreedly\Laravel\SpreedlyServiceProvider"
```

Add credentials to your `.env`:

```
SPREEDLY_ENVIRONMENT_KEY=your_environment_key
SPREEDLY_ACCESS_SECRET=your_access_secret
```

Use the facade:

```
use Laratusk\Spreedly\Laravel\Facades\Spreedly;

$gateway = Spreedly::gateways()->create(['gateway_type' => 'test']);
$transaction = Spreedly::transactions()->purchase($gateway->token, [
    'payment_method_token' => 'pm_token',
    'amount' => 1000,
    'currency_code' => 'USD',
]);
```

Or inject the client:

```
use Laratusk\Spreedly\SpreedlyClient;

class PaymentController extends Controller
{
    public function __construct(private readonly SpreedlyClient $spreedly) {}

    public function charge(Request $request)
    {
        $transaction = $this->spreedly->transactions->purchase(
            gatewayToken: config('spreedly.gateway_token'),
            params: [
                'payment_method_token' => $request->payment_method_token,
                'amount' => $request->amount, // in cents
                'currency_code' => 'USD',
            ],
        );

        if (! $transaction->succeeded) {
            throw new \Exception("Payment failed: {$transaction->message}");
        }

        return $transaction;
    }
}
```

Certificate Automation (Laravel)
--------------------------------

[](#certificate-automation-laravel)

Spreedly supports [certificate pinning](https://developer.spreedly.com/reference/certificates) for additional API security. The SDK can automatically generate, upload, and renew self-signed certificates on a per-machine basis, binding each certificate to the machine's MAC address so that multi-server deployments each maintain their own certificate.

### Setup

[](#setup)

Publish and run the migration:

```
php artisan vendor:publish --tag="spreedly-migrations"
php artisan migrate
```

Add the relevant variables to your `.env`:

```
# Optional: override MAC address auto-detection (e.g. in containerised environments)
SPREEDLY_MAC_ADDRESS=aa:bb:cc:dd:ee:ff

# Certificate settings (optional — shown with defaults):
SPREEDLY_CERTIFICATE_DAYS_VALID=365
SPREEDLY_CERTIFICATE_KEY_BITS=2048
SPREEDLY_CERTIFICATE_EXPIRING_DAYS=7
```

### How it works

[](#how-it-works)

Each server keeps exactly **one active certificate** at a time, identified by its MAC address. The key pair is generated locally (the private key never leaves the server), then uploaded to Spreedly. The encrypted private key is stored in your database.

ScenarioBehaviourNo certificate existsA new certificate is created and uploadedCertificate expires within threshold (default: 7 days)Certificate is renewed; old record is deletedCertificate is still validNo action taken`--force` flagCertificate is replaced immediately regardless of expiry### Artisan command

[](#artisan-command)

```
# Normal: renew only if expiring within the configured threshold
php artisan spreedly:certificate-install

# Force-replace the current certificate immediately
php artisan spreedly:certificate-install --force
```

### Scheduled auto-renewal

[](#scheduled-auto-renewal)

Register the command in your scheduler so certificates are renewed automatically. Running it once a day is sufficient — the command exits immediately when the certificate is not close to expiring.

**Laravel 11+ (`routes/console.php`):**

```
use Illuminate\Support\Facades\Schedule;

Schedule::command('spreedly:certificate-install')
    ->dailyAt('02:00')
    ->runInBackground()
    ->withoutOverlapping()
    ->onFailure(function () {
        // alert your team
    });
```

**Laravel 10 (`app/Console/Kernel.php`):**

```
protected function schedule(Schedule $schedule): void
{
    $schedule->command('spreedly:certificate-install')
        ->dailyAt('02:00')
        ->runInBackground()
        ->withoutOverlapping();
}
```

> **Tip:** Set `SPREEDLY_CERTIFICATE_EXPIRING_DAYS` to control how many days before expiry a renewal is triggered. The default is `7`.

### Resolving the current certificate

[](#resolving-the-current-certificate)

Retrieve the active certificate for the current machine at runtime:

```
use Laratusk\Spreedly\Laravel\Models\SpreedlyCertificate;

// Returns the certificate for this machine; creates one automatically if none exists.
$certificate = SpreedlyCertificate::current();

$certificate->getPem();           // PEM-encoded certificate body
$certificate->getPublicKey();     // RSA public key
$certificate->getPublicKeyHash(); // base64(sha256(publicKey)) — for TLS pinning
$certificate->getToken();         // Spreedly certificate token
$certificate->getPrivateKey();    // Decrypted private key PEM
```

---

Resources
---------

[](#resources)

### Gateways

[](#gateways)

> **Docs:** [Gateways API](https://developer.spreedly.com/reference/gateways)

```
// Create a gateway
$gateway = $spreedly->gateways->create([
    'gateway_type' => 'stripe',
    'login' => 'sk_test_xxx',
]);

// Retrieve
$gateway = $spreedly->gateways->retrieve('gateway_token');

// List (with pagination)
$gateways = $spreedly->gateways->list();
foreach ($gateways->autoPaginate() as $gateway) {
    echo $gateway->token;
}

// Update
$gateway = $spreedly->gateways->update('gateway_token', ['description' => 'New description']);

// Redact (removes sensitive credentials)
$spreedly->gateways->redact('gateway_token');

// Retain
$spreedly->gateways->retain('gateway_token');
```

### Payment Methods

[](#payment-methods)

> **Docs:** [Payment Methods API](https://developer.spreedly.com/reference/payment-methods)

```
// Create/tokenize (note: usually done via Spreedly Express or iframe)
$pm = $spreedly->paymentMethods->create([
    'credit_card' => [
        'number' => '4111111111111111',
        'month' => '12',
        'year' => '2025',
        'first_name' => 'John',
        'last_name' => 'Doe',
        'verification_value' => '123',
    ],
]);

// Retrieve
$pm = $spreedly->paymentMethods->retrieve('pm_token');

// List
$pms = $spreedly->paymentMethods->list();

// Update
$spreedly->paymentMethods->update('pm_token', ['first_name' => 'Jane']);

// Retain (prevent auto-removal), optionally provisioning a network token
$spreedly->paymentMethods->retain('pm_token');
$spreedly->paymentMethods->retain('pm_token', provisionNetworkToken: true);

// Redact (remove sensitive data)
$spreedly->paymentMethods->redact('pm_token');

// Recache CVV
$spreedly->paymentMethods->recache('pm_token', ['verification_value' => '456']);

// Copy the card into a gateway's own vault (third-party vaulting).
// The first argument is the gateway to store at, not the payment method.
$spreedly->paymentMethods->store('gw_token', ['payment_method_token' => 'pm_token']);

// Remove specific metadata keys
$spreedly->paymentMethods->deleteMetadata('pm_token', ['plan', 'campaign']);

// Filter the list
$pms = $spreedly->paymentMethods->list(
    state: 'retained',
    metadata: 'plan:pro',
    count: 100,
);
```

### Transactions

[](#transactions)

> **Docs:** [Transactions API](https://developer.spreedly.com/reference/transactions)
>
> **Note:** All monetary amounts are in the smallest currency unit (cents for USD). `1000` = $10.00.

```
// Purchase (charge immediately)
$purchase = $spreedly->transactions->purchase('gateway_token', [
    'payment_method_token' => 'pm_token',
    'amount' => 1000,          // $10.00 in cents
    'currency_code' => 'USD',
    'retain_on_success' => true,
]);

if ($purchase->succeeded) {
    echo "Charged: {$purchase->amount} {$purchase->currencyCode}";
}

// Authorize (reserve funds)
$auth = $spreedly->transactions->authorize('gateway_token', [
    'payment_method_token' => 'pm_token',
    'amount' => 1000,
    'currency_code' => 'USD',
]);

// Capture (charge a previous authorization)
$capture = $spreedly->transactions->capture($auth->token, ['amount' => 1000]);

// Void (cancel before settlement)
$void = $spreedly->transactions->void($purchase->token);

// Credit/Refund
$refund = $spreedly->transactions->credit($purchase->token, ['amount' => 500]); // partial refund

// General credit (not tied to existing transaction)
$spreedly->transactions->generalCredit('gateway_token', [
    'payment_method_token' => 'pm_token',
    'amount' => 1000,
    'currency_code' => 'USD',
]);

// Verify (zero-dollar authorization)
$spreedly->transactions->verify('gateway_token', [
    'payment_method_token' => 'pm_token',
]);

// Retrieve a transaction
$tx = $spreedly->transactions->retrieve('transaction_token');

// List transactions, optionally filtered by state and page size
$transactions = $spreedly->transactions->list(state: 'gateway_processing_failed', count: 100);

// Get transcript (raw gateway communication)
$transcript = $spreedly->transactions->transcript('transaction_token');

// Reference purchase — charge again against a previous transaction, reusing its
// payment method and stored credential details
$again = $spreedly->transactions->referencePurchase($purchase->token, ['amount' => 800]);
```

### Receivers

[](#receivers)

> **Docs:** [Receivers API](https://developer.spreedly.com/reference/receivers)

```
$receiver = $spreedly->receivers->create([
    'receiver_type' => 'oauth2_bearer',
    'credentials' => [
        ['name' => 'access_token', 'value' => 'token_here'],
    ],
    'hostnames' => ['api.example.com'],
]);

$receiver = $spreedly->receivers->retrieve('receiver_token');
$receivers = $spreedly->receivers->list();
$spreedly->receivers->update('receiver_token', [...]);
$spreedly->receivers->redact('receiver_token');
$spreedly->receivers->deliver('receiver_token', [...]);
```

### Certificates

[](#certificates)

> **Docs:** [Certificates API](https://developer.spreedly.com/reference/certificates)

```
$cert = $spreedly->certificates->create([...]);
$certs = $spreedly->certificates->list();
$spreedly->certificates->update('cert_token', [...]);

// Have Spreedly generate the key pair, so the private key never leaves their vault
$cert = $spreedly->certificates->generate([
    'algorithm' => 'ec-prime256v1',
    'cn' => 'MyApp ApplePay Production Certificate',
]);
```

### Environments

[](#environments)

> **Docs:** [Environments API](https://developer.spreedly.com/reference/environments)

```
$envs = $spreedly->environments->list();
$env = $spreedly->environments->create([...]);
$env = $spreedly->environments->retrieve('env_token');
$spreedly->environments->update('env_token', [...]);
$spreedly->environments->regenerateSigningSecret('env_key');
```

### Events

[](#events)

> **Docs:** [Events API](https://developer.spreedly.com/reference/events)

Environment events record what changed and which object it changed. They are identified by `id`, not a token.

```
$events = $spreedly->events->list(count: 100);

foreach ($events->items as $event) {
    echo "{$event->eventType} on {$event->objectType} {$event->objectKey}";
}

$event = $spreedly->events->retrieve($events->items[0]->id);
```

> Payment method events are a **different** resource with a different shape — see [Payment Method Events](#payment-method-events) below.

### Merchant Profiles

[](#merchant-profiles)

> **Docs:** [Merchant Profiles API](https://developer.spreedly.com/reference/merchant-profiles)

```
$profile = $spreedly->merchantProfiles->create([...]);
$profiles = $spreedly->merchantProfiles->list();
$profile = $spreedly->merchantProfiles->retrieve('token');
$spreedly->merchantProfiles->update('token', [...]);
```

### Composer (Workflows)

[](#composer-workflows)

> **Docs:** [Composer API](https://developer.spreedly.com/reference/composer)

```
$spreedly->composer->authorize([...]);
$spreedly->composer->purchase([...]);
$spreedly->composer->verify([...]);
```

### SCA Authentication

[](#sca-authentication)

> **Docs:** [SCA Authentication API](https://developer.spreedly.com/reference/sca-authentication)

```
$spreedly->scaAuthentication->authenticate('sca_provider_key', [
    'payment_method_token' => 'pm_token',
    'amount' => 1000,
    'currency_code' => 'EUR',
]);
```

### Asynchronous transactions (3DS2 and offsite)

[](#asynchronous-transactions-3ds2-and-offsite)

> **Docs:** [Gateway Specific 3DS2 Guide](https://developer.spreedly.com/docs/gateway-specific-3ds2-guide)

A transaction that needs the cardholder to authenticate comes back in the `pending`state carrying somewhere to send them:

```
$tx = $spreedly->transactions->purchase($gatewayToken, [
    'payment_method_token' => $token,
    'amount'               => 3004,
    'currency_code'        => 'EUR',
    'attempt_3dsecure'     => true,
    'three_ds_version'     => '2',
    'redirect_url'         => 'https://merchant.example/checkout/return',
    'callback_url'         => 'https://merchant.example/spreedly/callback',
    'browser_info'         => $browserInfo,
]);

if ($tx->requiresCardholderAction()) {
    // Hand the cardholder over, then complete the transaction afterwards.
    return redirect($tx->checkoutUrl);
}
```

PropertyDescription`checkoutUrl`Issuer page to send the cardholder to`checkoutForm`Pre-built form to post instead of redirecting`redirectUrl`Where the cardholder lands afterwards`callbackUrl`Where Spreedly POSTs state changes`setupResponse`Result of setting the transaction up on the gateway`redirectResponse`Result of the cardholder returning`callbackResponse`Result delivered out of bandFinish the transaction with `$spreedly->transactions->complete($tx->token)`.

The three sub-responses often hold error detail that the top-level `message` does not, which is what makes a failed authentication diagnosable.

### Callbacks and signed requests

[](#callbacks-and-signed-requests)

> **Docs:** [Signed requests](https://developer.spreedly.com/docs/signed-requests)

The redirect back to your site is not guaranteed to happen, so Spreedly also POSTs every transaction that changed to your `callback_url`. That channel is not authenticated, so the critical fields are signed with the environment's signing secret — never an access secret.

```
$transactions = Transaction::fromCallbackPayload($request->json()->all());

foreach ($transactions as $tx) {
    if (! $tx->verifySignature(config('services.spreedly.signing_secret'))) {
        abort(400);
    }

    // Safe to act on.
    $order->markPaid($tx->token);
}
```

`verifySignature()` recomputes the HMAC over exactly the fields the payload says were signed, and compares in constant time. An unsigned transaction returns `false`, so "not signed" and "signed but tampered with" fail the same way.

Roll the secret with `$spreedly->environments->regenerateSigningSecret('env_key')`.

### Raw payloads

[](#raw-payloads)

Every `Transaction` and `PaymentMethod` keeps the payload it was built from, so a field the SDK does not model yet is still reachable:

```
$tx->raw['some_new_field'];
$tx->paymentMethod->raw['third_party_token'];
```

Typed properties stay the supported surface; `raw` is the escape hatch for gateway-specific extras and for fields Spreedly adds between SDK releases.

### Sub Merchants

[](#sub-merchants)

> **Docs:** [Sub Merchants API](https://developer.spreedly.com/reference/sub-merchants)

```
$spreedly->subMerchants->create([...]);
$spreedly->subMerchants->list();
$spreedly->subMerchants->retrieve('token');
$spreedly->subMerchants->update('token', [...]);
```

### Card Refresher

[](#card-refresher)

> **Docs:** [Card Refresher API](https://developer.spreedly.com/reference/card-refresher)

Keeps stored payment methods up-to-date by fetching the latest card details from card networks.

```
// Submit a card for refreshing
$inquiry = $spreedly->cardRefresher->create([
    'payment_method_token' => 'pm_token',
    'region' => 'NA',
]);

// Retrieve an existing inquiry
$inquiry = $spreedly->cardRefresher->retrieve('inquiry_token');

// List all inquiries
$inquiries = $spreedly->cardRefresher->list();
```

### Claim

[](#claim)

> **Docs:** [Claim API](https://developer.spreedly.com/reference/claim)

Forward a chargeback claim for a transaction to its protection provider.

```
$result = $spreedly->claim->create('transaction_token', [
    'reason_type' => 'FRAUD',
    'amount' => 1000,
    'currency' => 'USD',
]);
```

### Payments

[](#payments)

> **Docs:** [Payments API](https://developer.spreedly.com/reference/payments)

```
$payment = $spreedly->payments->retrieve('payment_token');
```

### Protection Events

[](#protection-events)

> **Docs:** [Protection Events API](https://developer.spreedly.com/reference/protection-events)

Protection events are created when Spreedly detects a change to a stored payment method (e.g. updated card number or expiration date).

```
// List all protection events
$events = $spreedly->protectionEvents->list(state: 'succeeded', count: 100);

// Retrieve a specific event
$event = $spreedly->protectionEvents->retrieve('event_token');

echo $event->eventType;           // e.g. 'card_updated'
echo $event->paymentMethodToken;
```

### Access Secrets (Environments)

[](#access-secrets-environments)

> **Docs:** [Access Secrets API](https://developer.spreedly.com/reference/access-secrets)

```
// Create an access secret for an environment
$secret = $spreedly->environments->createAccessSecret('env_token', [
    'name' => 'Production Key',
    'description' => 'Used by the payments service',
]);

// List all access secrets
$secrets = $spreedly->environments->listAccessSecrets('env_token');

// Retrieve a specific access secret
$secret = $spreedly->environments->retrieveAccessSecret('env_token', 'secret_token');

// Delete an access secret
$spreedly->environments->deleteAccessSecret('env_token', 'secret_token');
```

### Network Tokenization (Payment Methods)

[](#network-tokenization-payment-methods)

> **Docs:** [Network Tokenization API](https://developer.spreedly.com/reference/network-tokenization)

```
// Get network token metadata
$metadata = $spreedly->paymentMethods->networkTokenizationMetadata('pm_token');

// Get network token status
$status = $spreedly->paymentMethods->networkTokenizationStatus('pm_token');
```

### Payment Method Events

[](#payment-method-events)

> **Docs:** [Payment Method Events API](https://developer.spreedly.com/reference/payment-method-events)

```
// List all payment method events (across all payment methods)
$events = $spreedly->paymentMethods->listEvents(eventType: 'card_updated', includeTransactions: true);

// List events for a specific payment method
$events = $spreedly->paymentMethods->listEventsForPaymentMethod('pm_token');

// Retrieve a specific event; these are PaymentMethodEvent, not Event
$event = $spreedly->paymentMethods->retrieveEvent('event_token');

echo $event->eventType;         // e.g. 'card_updated'
echo $event->paymentMethodKey;
$event->eventData;              // what changed

// Update a payment method without a charge (gratis)
$pm = $spreedly->paymentMethods->updateGratis('pm_token', [
    'month' => '12',
    'year'  => '2027',
]);
```

### Protection Provider &amp; SCA Provider (Merchant Profiles)

[](#protection-provider--sca-provider-merchant-profiles)

> **Docs:** [Merchant Profiles API](https://developer.spreedly.com/reference/merchant-profiles)

```
Providers are created on a merchant profile but are retrieved by their own token.
At least one card type object must be given.

```php
// Protection provider
$provider = $spreedly->merchantProfiles->createProtectionProvider('mp_token', [
    'type' => 'spreedly',
    'visa' => [...],
]);
$spreedly->merchantProfiles->retrieveProtectionProvider($provider['protection_provider']['token']);

// SCA provider
$sca = $spreedly->merchantProfiles->createScaProvider('mp_token', [
    'type' => 'spreedly',
    'visa' => [...],
]);
$spreedly->merchantProfiles->retrieveScaProvider($sca['sca_provider']['token']);
```

Pagination
----------

[](#pagination)

Spreedly uses token-based pagination (`since_token`). The SDK provides a `PaginatedCollection` that handles this:

```
// Fetch first page
$gateways = $spreedly->gateways->list();

// Fetch next page manually
$nextPage = $gateways->nextPage();

// Auto-paginate through all pages (lazy generator)
foreach ($gateways->autoPaginate() as $gateway) {
    echo $gateway->token . "\n";
}

// Standard iteration (current page only)
foreach ($gateways as $gateway) {
    echo $gateway->token . "\n";
}

// Count items on current page
echo count($gateways);
```

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

[](#error-handling)

```
use Laratusk\Spreedly\Exceptions\AuthenticationException;
use Laratusk\Spreedly\Exceptions\InvalidRequestException;
use Laratusk\Spreedly\Exceptions\NotFoundException;
use Laratusk\Spreedly\Exceptions\RateLimitException;
use Laratusk\Spreedly\Exceptions\ApiException;
use Laratusk\Spreedly\Exceptions\TimeoutException;
use Laratusk\Spreedly\Exceptions\SpreedlyException;

try {
    $gateway = $spreedly->gateways->retrieve('invalid_token');
} catch (AuthenticationException $e) {
    // 401 - Invalid credentials
    echo $e->getMessage();
} catch (NotFoundException $e) {
    // 404 - Resource not found
    echo $e->getMessage();
} catch (InvalidRequestException $e) {
    // 422 - Validation errors
    foreach ($e->errors as $error) {
        echo $error['message'];
    }
} catch (RateLimitException $e) {
    // 429 - Too many requests
    sleep(1);
} catch (ApiException $e) {
    // 500+ - Server error
    echo "Status: {$e->httpStatus}";
} catch (TimeoutException $e) {
    // Connection timeout
} catch (SpreedlyException $e) {
    // Any other Spreedly error
}
```

All exceptions extend `SpreedlyException` and provide:

- `$e->getMessage()` — Human-readable message
- `$e->httpStatus` — HTTP status code
- `$e->errors` — Array of validation errors (for 422)
- `$e->spreedlyErrorKey` — Spreedly error key (e.g., `errors.not_found`)

Custom HTTP Transport
---------------------

[](#custom-http-transport)

Implement `TransporterInterface` to use a custom HTTP client:

```
use Laratusk\Spreedly\Contracts\TransporterInterface;

class MyTransporter implements TransporterInterface
{
    public function get(string $endpoint, array $query = []): array { ... }
    public function post(string $endpoint, array $payload = []): array { ... }
    public function put(string $endpoint, array $payload = []): array { ... }
    public function patch(string $endpoint, array $payload = []): array { ... }
    public function delete(string $endpoint, array $query = []): array { ... }
    public function getRaw(string $endpoint): string { ... }
}

$spreedly = new SpreedlyClient(
    environmentKey: 'key',
    accessSecret: 'secret',
    transporter: new MyTransporter(),
);
```

Testing
-------

[](#testing)

### Testing in Your Application

[](#testing-in-your-application)

The SDK ships with `SpreedlyFake` and `MockTransporter` to make testing easy — no real HTTP calls, no Spreedly credentials needed.

#### Standalone PHP

[](#standalone-php)

```
use Laratusk\Spreedly\Testing\SpreedlyFake;

$fake = SpreedlyFake::make();

// Configure responses before making calls
$fake->mock->addResponse('GET', 'gateways/gw_token.json', [
    'gateway' => [
        'token'        => 'gw_token',
        'gateway_type' => 'test',
        'name'         => 'Test',
        'state'        => 'retained',
        // ...
    ],
]);

$gateway = $fake->client()->gateways->retrieve('gw_token');

assert($gateway->token === 'gw_token');

// Assert that the expected call was made
$fake->mock->assertCalled('GET', 'gateways/gw_token.json');

// Count how many calls were made
echo $fake->mock->getCallCount(); // 1
```

#### Laravel (swap the container binding)

[](#laravel-swap-the-container-binding)

In your Laravel feature tests, swap the `SpreedlyClient` binding before the code under test runs. After the swap the `Spreedly` facade automatically uses the fake.

```
use Laratusk\Spreedly\Laravel\Facades\Spreedly;
use Laratusk\Spreedly\SpreedlyClient;
use Laratusk\Spreedly\Testing\SpreedlyFake;

class PaymentTest extends TestCase
{
    public function test_purchase_succeeds(): void
    {
        $fake = SpreedlyFake::make();

        // Register a canned response for the endpoint your code will hit
        $fake->mock->addResponse('POST', 'gateways/gw_token/purchase.json', [
            'transaction' => [
                'token'            => 'tx_abc123',
                'transaction_type' => 'Purchase',
                'succeeded'        => true,
                'amount'           => 1000,
                'currency_code'    => 'USD',
                'state'            => 'succeeded',
                'message'          => 'Succeeded!',
                'created_at'       => now()->toIso8601String(),
                'updated_at'       => now()->toIso8601String(),
            ],
        ]);

        // Swap the real client for the fake one
        $this->app->instance(SpreedlyClient::class, $fake->client());

        // Call your application code (which uses the Spreedly facade internally)
        $response = $this->postJson('/api/charge', [
            'payment_method_token' => 'pm_token',
            'amount'               => 1000,
        ]);

        $response->assertOk();

        // Verify Spreedly was actually called
        $fake->mock->assertCalled('POST', 'gateways/gw_token/purchase.json');
    }
}
```

Or test the facade directly:

```
$fake = SpreedlyFake::make();
$fake->mock->addResponse('GET', 'gateways/gw_token.json', ['gateway' => [...]]);

$this->app->instance(SpreedlyClient::class, $fake->client());

$gateway = Spreedly::gateways()->retrieve('gw_token');

expect($gateway->token)->toBe('gw_token');
$fake->mock->assertCalled('GET', 'gateways/gw_token.json');
```

#### `MockTransporter` API

[](#mocktransporter-api)

MethodDescription`addResponse(method, endpoint, array)`Register a canned response. Chainable.`assertCalled(method, endpoint)`Throws `RuntimeException` if the call was never made.`getCallCount()`Total number of HTTP calls recorded.---

### Running the SDK's Own Tests

[](#running-the-sdks-own-tests)

Run tests:

```
composer test
```

Run quality checks:

```
composer quality
```

### Integration Tests

[](#integration-tests)

Unit tests mock the transporter, so they can only confirm that the SDK sends the path the test already expects. The integration suite calls the real API and proves that every endpoint the SDK addresses is one Spreedly actually serves.

It creates a test gateway, a payment method and a transaction first, then exercises each endpoint with tokens that genuinely exist — so a `404` can only mean the path is wrong, never that a record is missing. A `422` passes: the route was understood and the body rejected, which is all the assertion is about.

```
SPREEDLY_INTEGRATION=true \
SPREEDLY_ENVIRONMENT_KEY=your_key \
SPREEDLY_ACCESS_SECRET=your_secret \
vendor/bin/pest --testsuite Integration
```

Without `SPREEDLY_INTEGRATION=true` the suite skips, so CI stays green without credentials.

Use a **test/sandbox environment**. The suite creates real records (a gateway, a payment method, transactions, a merchant profile, a certificate) and does not clean them up.

Nothing destructive runs. Spreedly answers `401` for a route that exists but is out of scope for the credentials and `404` for one that does not exist, so endpoints that cannot be called safely — regenerating the signing secret would invalidate every callback signature already issued — are proven by that distinction instead.

Organization-scoped endpoints (sub merchants, environments) skip with a clear message when only environment credentials are supplied.

License
-------

[](#license)

MIT. See [LICENSE.md](LICENSE.md).

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance98

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 84.6% 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 ~27 days

Recently: every ~36 days

Total

7

Last Release

10d ago

Major Versions

v1.4.0 → v2.0.02026-08-08

### Community

Maintainers

![](https://www.gravatar.com/avatar/755245aa5e4ba6c690b039cedcce5a86cd01b4f00d490cd71f03e6377ac302d5?d=identicon)[laratusk](/maintainers/laratusk)

---

Top Contributors

[![azer1ghost](https://avatars.githubusercontent.com/u/27803185?v=4)](https://github.com/azer1ghost "azer1ghost (11 commits)")[![elvinaqalarov99](https://avatars.githubusercontent.com/u/64695589?v=4)](https://github.com/elvinaqalarov99 "elvinaqalarov99 (2 commits)")

---

Tags

phplaravelsdkpaymentsspreedlypayment-orchestration

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/laratusk-spreedly/health.svg)

```
[![Health](https://phpackages.com/badges/laratusk-spreedly/health.svg)](https://phpackages.com/packages/laratusk-spreedly)
```

###  Alternatives

[laravel/framework

The Laravel Framework.

34.9k556.2M21.5k](/packages/laravel-framework)[statamic/cms

The Statamic CMS Core Package

4.9k3.8M1.2k](/packages/statamic-cms)[bagisto/bagisto

Bagisto Laravel E-Commerce

28.0k175.2k9](/packages/bagisto-bagisto)[eslazarev/wildberries-sdk

Wildberries OpenAPI clients (generated).

353.6k](/packages/eslazarev-wildberries-sdk)[chargebee/chargebee-php

ChargeBee API client implementation for PHP

758.7M10](/packages/chargebee-chargebee-php)[lion/bundle

Lion-framework configuration and initialization package

132.4k5](/packages/lion-bundle)

PHPackages © 2026

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