Search by

payscribe / php-sdk

sokoyaphilip

Official Payscribe PHP SDK — virtual accounts, virtual cards, bills, payouts, customers and webhooks.

Package info

github.com/payscribe/php-sdk

pkg:composer/payscribe/php-sdk

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 2

Open Issues: 1

v1.0.1 2026-08-03 12:33 UTC

This package is auto-updated.

Last update: 2026-09-30 06:17:04 UTC


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-json
  • ext-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::$number and Card::$ccv are private — accessed only via getNumber() and getCvv(). var_dump() / json_encode() / logging redact them automatically (__debugInfo, JsonSerializable). Use $card->getMaskedNumber() for display.
  • Config::$secretKey and Webhooks secret key are also redacted in var_dump() to prevent credential leakage in debug output.
  • Exception context (raw API response in $e->getContext()) has sensitive fields like number, ccv, pan redacted 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 baseUrl in Config must use https://. 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