payscribe / php-sdk
Official Payscribe PHP SDK — virtual accounts, virtual cards, bills, payouts, customers and webhooks.
Requires
- php: >=8.1
- guzzlehttp/guzzle: ^7.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.0
- vlucas/phpdotenv: ^5.6
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Virtual accounts, virtual cards, bill payments, and webhook verification.
Features
- Static/permanent NGN virtual account creation
- Dynamic/temporary NGN virtual account creation
- Virtual account lookup, activation, deactivation
- Payment confirmation and sandbox transfer simulation
- USD virtual card creation (including stablecoin-funded)
- Card top-up, withdrawal, replacement, freezing, unfreezing, termination
- Card transaction history and billing contact updates
- Bill payments: cable TV, internet, data, E-pins, electricity, airtime, SMS, betting, international
- Webhook signature verification (HMAC-SHA256)
- Automatic retry with exponential backoff
- Sensitive data redaction in logs and error context
Requirements
- PHP 8.1+
- Composer
ext-jsonext-curl
Installation
composer require payscribe/php-sdk
Quickstart
<?php require __DIR__ . '/vendor/autoload.php'; use Payscribe\Payscribe; $payscribe = Payscribe::sandbox('sk_test_your_key'); // 1. Create a customer first (returns array) $customer = $payscribe->customers->create( firstName: 'John', lastName: 'Doe', email: 'john@example.com', phone: '+2348012345678', ); $customerId = $customer['customer_id'] ?? ''; // 2. Create a static virtual account $account = $payscribe->virtualAccounts->createStatic($customerId, ['9PSB']); echo $account->accountNumber; // 5031240100 // 3. Create a virtual card $card = $payscribe->virtualCards->create( customerId: $customerId, currency: 'USD', brand: 'visa', amount: 100, reference: 'ref_' . uniqid(), ); echo $card->getMaskedNumber(); // 411111******1111 (safe for display) echo $card->getNumber(); // 4111111111111111 (full PAN — use carefully)
Environment
| Method | Environment |
|---|---|
Payscribe::sandbox($key) |
Sandbox (test) |
Payscribe::production($key) |
Live |
Configuration
$payscribe = new Payscribe( new \Payscribe\Type\Config( secretKey: 'sk_test_xxx', // required baseUrl: 'https://custom.api.com', // optional, overrides environment URL timeout: 30, // optional, default 30s maxRetries: 3, // optional, default 3 (exponential backoff) ), $yourPsr18Client, // optional, must implement Psr\Http\Client\ClientInterface );
Resources
$payscribe->customers
| Method | Returns |
|---|---|
create(string $firstName, string $lastName, string $email, string $phone, string $country = 'NG'): array |
array with customer_id |
Create a customer before issuing accounts or cards:
$customer = $payscribe->customers->create( firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com', phone: '+2348012345678', ); $customerId = $customer['customer_id'] ?? $customer['id'] ?? '';
$payscribe->virtualAccounts
| Method | Returns |
|---|---|
createStatic(string $customerId, array $banks, string $currency = 'NGN', ?string $identityType = null, ?string $identityNumber = null): VirtualAccount |
VirtualAccount{accountNumber, bank, currency, customerId, ...} |
createDynamic(string $reference, float $amount, string $amountType, int $expiresDuration, string $expiresType, string $customerName, string $customerEmail, string $customerPhone, string $currency = 'NGN', ?string $description = null): VirtualAccount |
Same shape |
get(string $accountNumber): VirtualAccount |
Single account |
activate(string $accountNumber): ActionResponse |
Activate an account |
deactivate(string $accountNumber): ActionResponse |
Deactivate an account |
confirmPayment(string $sessionId, float $amount, string $accountNumber, ?string $transId = null): ActionResponse |
Confirm a pending payment |
simulateTransfer(string $reference, float $amount, string $account, string $name, string $bank, string $senderAccountNumber, string $senderName, string $hash, ?string $description = null, string $currency = 'NGN'): ActionResponse |
Sandbox only; simulates an incoming transfer |
// Static account (permanent, reusable) $account = $payscribe->virtualAccounts->createStatic( customerId: 'cus_xxx', banks: ['9PSB'], ); echo $account->accountNumber; // Dynamic account (checkout with expiry) $account = $payscribe->virtualAccounts->createDynamic( reference: 'order_123', amount: 2500, amountType: 'EXACT', expiresDuration: 30, expiresType: 'minutes', customerName: 'Ada Lovelace', customerEmail: 'ada@example.com', customerPhone: '08099228833', );
$payscribe->virtualCards
| Method | Returns |
|---|---|
create(string $customerId, string $currency, string $brand, float $amount, string $reference, string $type = 'virtual', ?bool $contactless, ?float $dailyLimit, ?float $transactionLimit): Card |
Card{id, brand, currency, lastFour, getMaskedNumber(), ...} |
createWithStablecoin(...same..., string $stablecoinCurrency = 'USDT', string $stablecoinNetwork = 'Tron', string $stablecoinChain = 'TRC20'): Card |
Card (funded via stablecoin deposit) |
topUp(string $cardId, float $amount, ?string $reference = null): ActionResponse |
Add funds |
topUpWithStablecoin(string $cardId, float $amount, string $stablecoinCurrency = 'USDT', string $stablecoinNetwork = 'Tron', string $stablecoinChain = 'TRC20', ?string $reference = null): ActionResponse |
Add funds via crypto |
withdraw(string $cardId, float $amount, string $reference): ActionResponse |
Withdraw funds |
get(string $cardId): Card |
Single card details |
replace(string $cardId): Card |
Replace lost/stolen card |
regularize(string $cardId): Card |
Alias for replace |
transactions(string $cardId, string $startDate, string $endDate, ?int $page, ?int $pageSize): array |
['data' => Transaction[], 'total' => int, 'page' => int] |
freeze(string $cardId, string $reference): ActionResponse |
Freeze card |
unfreeze(string $cardId, string $reference): ActionResponse |
Unfreeze card |
terminate(string $cardId, string $reference): ActionResponse |
Close card permanently |
updateContact(string $cardId, ?string $email, ?string $mobile, ?string $address1, ?string $address2, ?string $city, ?string $state, ?string $zipcode, ?string $country): ActionResponse |
Update billing contact |
Valid currencies: USD, NGN, GBP, EUR
Valid brands: visa, mastercard
// Input validation throws InvalidArgumentException early: $card = $payscribe->virtualCards->create( customerId: 'cus_xxx', currency: 'DOGE', // throws InvalidArgumentException: "Invalid currency 'DOGE'. Supported: USD, NGN, GBP, EUR" brand: 'visa', amount: 100, reference: 'ref_xxx', );
$payscribe->bills
Bills are exposed under $payscribe->bills, grouped by product category. Every method returns the unwrapped API payload as an array.
| Resource | Method | Endpoint |
|---|---|---|
requery |
requery(string $transactionId) |
GET requery |
cable |
fetchBouquets(string $service) |
GET bouquets |
cable |
validateSmartCard(string $service, string $account, string $planId, ?int $month = null) |
POST multichoice/validate |
cable |
pay(string $planId, string $customerName, string $account, string $service, string $reference, ?string $phone, ?string $email, ?int $month) |
POST multichoice/vend |
cable |
topUp(float|string $amount, string $customerName, string $account, string $service, string $reference, ?string $phone, ?string $email, ?int $month) |
POST multichoice/topup |
internet |
listServices() |
GET internet/list |
internet |
spectranetPinPlans() |
GET internet/spectranet/pins/plans |
internet |
purchaseSpectranetPins(string $planId, int $quantity, string $reference) |
POST internet/spectranet/pins/vend |
data |
lookup(?string $network, ?string $category) |
GET data/lookup |
data |
vend(string $plan, string|array $recipient, string $network, ?string $reference) |
POST data/vend |
epins |
list() |
GET epins |
epins |
purchase(string $id, int $quantity, string $reference) |
POST epins/vend |
epins |
lookupJambUser(int|string $id, string $account) |
POST epins/jamb/user/lookup |
epins |
retrieve(string $transactionId) |
GET epins/retrieve |
airtimeToWallet |
lookup() |
POST airtime_to_wallet |
airtimeToWallet |
process(string $network, float|string $amount, string $phoneNumber, string $from, ?string $reference) |
POST airtime_to_wallet/vend |
electricity |
validate(string $meterNumber, string $meterType, float|string $amount, string $service) |
POST electricity/validate |
electricity |
pay(string $meterNumber, string $meterType, float|string $amount, string $service, string $customerName, ?string $address, ?string $phone, ?string $email, ?string $reference) |
POST electricity/vend |
sms |
send(string $to, string $message, ?string $reference, ?string $senderId) |
POST sms |
betting |
listProviders() |
GET betting/list |
betting |
lookup(string $betId, string $customerId) |
GET betting/lookup |
betting |
fundWallet(string $betId, string $customerId, string $customerName, float|string $amount, string $reference) |
POST betting/vend |
airtime |
vend(string $network, float|string $amount, string|array $recipient, ?bool $ported, ?string $reference) |
POST airtime |
airtime |
bulkVend(array $rows, ?string $reference) |
POST airtime |
international |
countries() |
GET international-bills/countries |
international |
providers(string $iso) |
GET international-bills/providers |
international |
products(string $iso, string $code) |
GET international-bills/products |
international |
rate(string $iso, string $sku, float|string $amount) |
GET international-bills/rate |
international |
vend(string $iso, string $providerCode, string $sku, float|string $amount, string $account, string $reference, ?string $debitCurrency) |
POST international-bills/vend |
// Airtime $result = $payscribe->bills->airtime->vend( network: 'mtn', amount: 500, recipient: '08030000000', reference: 'ref_' . uniqid(), ); // Electricity $meter = $payscribe->bills->electricity->validate('1234567890', 'prepaid', 2000, 'ikedc'); $vend = $payscribe->bills->electricity->pay( meterNumber: '1234567890', meterType: 'prepaid', amount: 2000, service: 'ikedc', customerName: $meter['name'] ?? '', reference: 'ref_' . uniqid(), ); // Data $plans = $payscribe->bills->data->lookup(network: 'mtn', category: 'data'); $data = $payscribe->bills->data->vend('data_plan_1', '08030000000', 'mtn', 'ref_' . uniqid()); // Requery a transaction $status = $payscribe->bills->requery($transactionId);
$payscribe->webhooks
Verify webhook payloads using HMAC-SHA256. Use the raw request body (not re-encoded JSON).
| Method | Returns |
|---|---|
verifySignature(string $rawBody, string $signature, ?int $timestamp = null, int $tolerance = 300): bool |
true if signature matches |
constructEvent(string $rawBody, ?string $signature, ?int $timestamp = null, int $tolerance = 300): array |
Parsed event array, throws on invalid signature |
Pass a Unix $timestamp to activate replay protection — the SDK signs {timestamp}.{payload} and rejects payloads older than $tolerance seconds (default 5 min). Legacy mode works without a timestamp.
// Simple verification (no replay protection) $isValid = $payscribe->webhooks->verifySignature( $rawBody, $_SERVER['HTTP_X_PAYSCRIBE_SIGNATURE'], ); // With replay protection (recommended) $event = $payscribe->webhooks->constructEvent( $rawBody, $signature, timestamp: time(), // signed into the HMAC tolerance: 300, // reject if older than 5 min ); ## Error handling All API errors throw typed exceptions. Catch them individually or use the base class: ```php use Payscribe\Exception\{ PayscribeException, AuthException, ValidationException, RateLimitException, ServerException, }; try { $card = $payscribe->virtualCards->get('crd_unknown'); } catch (AuthException $e) { echo 'Check your secret key'; } catch (ValidationException $e) { echo 'Validation failed: ' . $e->getMessage(); } catch (RateLimitException $e) { $retryAfter = $e->getContext()['retry_after'] ?? 1; sleep($retryAfter); // retry } catch (ServerException $e) { echo 'Server error, will be retried automatically: ' . $e->getMessage(); } catch (PayscribeException $e) { echo "API error ({$e->getCode()}): {$e->getMessage()}"; }
| Exception | HTTP Status |
|---|---|
AuthException |
401 |
ValidationException |
422 |
RateLimitException |
429 |
ServerException |
5xx |
PayscribeException |
All other errors |
Retry & Timeouts
By default the SDK retries on network failures and 5xx responses with exponential backoff (up to 3 retries). Configure via Config:
$config = new \Payscribe\Type\Config( secretKey: 'sk_test_xxx', timeout: 60, // request timeout in seconds maxRetries: 5, // max retry attempts );
Security notes
Card::$numberandCard::$ccvare private — accessed only viagetNumber()andgetCvv().var_dump()/json_encode()/ logging redact them automatically (__debugInfo,JsonSerializable). Use$card->getMaskedNumber()for display.Config::$secretKeyandWebhookssecret key are also redacted invar_dump()to prevent credential leakage in debug output.- Exception context (raw API response in
$e->getContext()) has sensitive fields likenumber,ccv,panredacted automatically. - Webhook replay protection — pass a Unix timestamp to
verifySignature()/constructEvent(). The SDK signs{timestamp}.{payload}and rejects payloads older than 5 minutes (configurable via$tolerance). - HTTPS enforced — custom
baseUrlinConfigmust usehttps://. Built-in environment URLs are always HTTPS. - Never commit your secret key — use environment variables:
$key = getenv('PAYSCRIBE_SECRET_KEY'); $payscribe = Payscribe::production($key);
- Run the security verification script to validate all protections:
php security_verify.php
Testing
# All unit tests (mocked HTTP, no API key required) vendor/bin/phpunit # Integration tests (requires sandbox key in .env) PAYSCRIBE_SECRET_KEY=sk_test_xxx vendor/bin/phpunit --testsuite=Integration # Security redaction verification php security_verify.php # With coverage php -d xdebug.mode=coverage vendor/bin/phpunit --coverage-text
Legacy Bill Payments
Deprecated — use the native $payscribe->bills resource above instead. For reference, main.php contains the legacy bill-payment methods.
$payscribe = Payscribe::createFromEnv(); $account = $payscribe->account(); $payscribe->vendAirtime('mtn', 100, '08030000000');
License
MIT