A production-ready PHP SDK for the Spreedly payment orchestration API, following the Stripe PHP SDK architecture. Works as a standalone PHP library or as a Laravel package.
- PHP ^8.2
- Laravel ^10.0 || ^11.0 || ^12.0 (optional)
composer require laratusk/spreedly$spreedly = new \Laratusk\Spreedly\SpreedlyClient(
environmentKey: 'your_environment_key',
accessSecret: 'your_access_secret',
);$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,
],
);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_secretUse 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;
}
}Spreedly supports certificate pinning 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.
Publish and run the migration:
php artisan vendor:publish --tag="spreedly-migrations"
php artisan migrateAdd 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=7Each 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.
| Scenario | Behaviour |
|---|---|
| No certificate exists | A new certificate is created and uploaded |
| Certificate expires within threshold (default: 7 days) | Certificate is renewed; old record is deleted |
| Certificate is still valid | No action taken |
--force flag |
Certificate is replaced immediately regardless of expiry |
# 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 --forceRegister 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_DAYSto control how many days before expiry a renewal is triggered. The default is7.
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 PEMDocs: Gateways API
// 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');Docs: Payment Methods API
// 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)
$spreedly->paymentMethods->retain('pm_token');
// Redact (remove sensitive data)
$spreedly->paymentMethods->redact('pm_token');
// Recache CVV
$spreedly->paymentMethods->recache('pm_token', ['verification_value' => '456']);
// Store at gateway
$spreedly->paymentMethods->store('pm_token', ['gateway_token' => 'gw_token']);Docs: Transactions API
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
$transactions = $spreedly->transactions->list();
// Get transcript (raw gateway communication)
$transcript = $spreedly->transactions->transcript('transaction_token');Docs: Receivers API
$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', [...]);Docs: Certificates API
$cert = $spreedly->certificates->create([...]);
$certs = $spreedly->certificates->list();
$spreedly->certificates->update('cert_token', [...]);
$spreedly->certificates->generate('cert_token');Docs: Environments API
$envs = $spreedly->environments->list();
$env = $spreedly->environments->create([...]);
$env = $spreedly->environments->retrieve('env_token');
$spreedly->environments->update('env_token', [...]);
$spreedly->environments->regenerateSigningSecret();Docs: Events API
$events = $spreedly->events->list();
$event = $spreedly->events->retrieve('event_token');Docs: Merchant Profiles API
$profile = $spreedly->merchantProfiles->create([...]);
$profiles = $spreedly->merchantProfiles->list();
$profile = $spreedly->merchantProfiles->retrieve('token');
$spreedly->merchantProfiles->update('token', [...]);Docs: Composer API
$spreedly->composer->authorize([...]);
$spreedly->composer->purchase([...]);
$spreedly->composer->verify([...]);Docs: SCA Authentication API
$spreedly->scaAuthentication->authenticate([...]);Docs: Sub Merchants API
$spreedly->subMerchants->create([...]);
$spreedly->subMerchants->list();
$spreedly->subMerchants->retrieve('token');
$spreedly->subMerchants->update('token', [...]);Docs: Card Refresher API
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',
]);
// Retrieve an existing inquiry
$inquiry = $spreedly->cardRefresher->retrieve('inquiry_token');
// List all inquiries
$inquiries = $spreedly->cardRefresher->list();Docs: Claim API
$result = $spreedly->claim->create([
'payment_method_token' => 'pm_token',
]);Docs: Payments API
$payment = $spreedly->payments->retrieve('payment_token');Docs: Protection Events API
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();
// Retrieve a specific event
$event = $spreedly->protectionEvents->retrieve('event_token');
echo $event->eventType; // e.g. 'card_updated'
echo $event->paymentMethodToken;Docs: Access Secrets API
// 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');Docs: Network Tokenization API
// Get network token metadata
$metadata = $spreedly->paymentMethods->networkTokenizationMetadata('pm_token');
// Get network token status
$status = $spreedly->paymentMethods->networkTokenizationStatus('pm_token');// List all payment method events (across all payment methods)
$events = $spreedly->paymentMethods->listEvents();
// List events for a specific payment method
$events = $spreedly->paymentMethods->listEventsForPaymentMethod('pm_token');
// Retrieve a specific event
$event = $spreedly->paymentMethods->retrieveEvent('event_token');
// Update a payment method without a charge (gratis)
$pm = $spreedly->paymentMethods->updateGratis('pm_token', [
'month' => '12',
'year' => '2027',
]);Docs: Merchant Profiles API
// Protection provider
$spreedly->merchantProfiles->createProtectionProvider('mp_token', [
'provider_type' => 'kount',
]);
$spreedly->merchantProfiles->retrieveProtectionProvider('mp_token');
// SCA provider
$spreedly->merchantProfiles->createScaProvider('mp_token', [
'provider_type' => 'stripe_radar',
]);
$spreedly->merchantProfiles->retrieveScaProvider('mp_token');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);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)
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(),
);The SDK ships with SpreedlyFake and MockTransporter to make testing easy — no real HTTP calls, no Spreedly credentials needed.
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(); // 1In 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');| Method | Description |
|---|---|
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. |
Run tests:
composer testRun quality checks:
composer qualityIntegration tests require real Spreedly credentials and run against the test gateway:
SPREEDLY_INTEGRATION=true \
SPREEDLY_ENVIRONMENT_KEY=your_key \
SPREEDLY_ACCESS_SECRET=your_secret \
composer test -- --testsuite IntegrationMIT. See LICENSE.md.