payintohq / payinto-php-sdk
PHP SDK for the Payinto Business and Checkout APIs.
Requires
- php: >=8.1
- guzzlehttp/guzzle: ^7.8 || ^8.0
Requires (Dev)
- orchestra/testbench: ^8.0 || ^9.0 || ^10.0 || ^11.0
- phpunit/phpunit: ^10.5 || ^11.0 || ^12.0
README
PHP 8.1+ SDK for the Payinto Business API and Checkout API v1.
Installation
composer require payintohq/payinto-php-sdk
use Payinto\Client; $payinto = new Client([ 'secretKey' => $_ENV['PAYINTO_LIVE_SECRET_KEY'], 'publicKey' => $_ENV['PAYINTO_LIVE_PUBLIC_KEY'] ?? null, 'webhookSecret' => $_ENV['PAYINTO_LIVE_WEBHOOK_SECRET'] ?? null, ]);
The SDK uses https://api.payinto.co for both production and sandbox credentials. Select the environment with the credential you provide; no base URL setting is required.
Secret keys (sk_) belong only on your server. Public keys (pk_) are suitable for Checkout initialization flows. Amounts use the lowest currency unit: NGN amounts are kobo.
Laravel
Laravel applications receive the service provider and facade through Composer package auto-discovery. Publish the package configuration with:
php artisan vendor:publish --tag=payinto-config
Define both live and test credentials in your .env file, then select the active credential pair with PAYINTO_ENV:
PAYINTO_ENV=live PAYINTO_LIVE_SECRET_KEY=sk_live_your_secret_key PAYINTO_LIVE_PUBLIC_KEY=pk_live_your_public_key PAYINTO_LIVE_WEBHOOK_SECRET=whsec_live_your_webhook_secret PAYINTO_TEST_SECRET_KEY=sk_test_your_secret_key PAYINTO_TEST_PUBLIC_KEY=pk_test_your_public_key PAYINTO_TEST_WEBHOOK_SECRET=whsec_test_your_webhook_secret PAYINTO_TIMEOUT=60 PAYINTO_WEBHOOK_TOLERANCE=300
Use PAYINTO_ENV=test when testing. The API host remains https://api.payinto.co; the selected credential and webhook-secret pair changes.
PAYINTO_WEBHOOK_TOLERANCE controls the accepted timestamp window in seconds. The default is five minutes. Set it to 0 to disable timestamp checking while retaining HMAC verification.
Inject Payinto\Client into a class or use the Payinto facade:
use Payinto\Client; public function show(Client $payinto): array { return $payinto->business->whoami()->raw(); }
Responses and errors
Every successful call returns ApiResponse:
$response = $payinto->business->whoami(); $business = $response->data(); $response->status(); $response->message(); $response->timestamp(); $response->raw(); // complete decoded envelope
Non-2xx responses throw AuthenticationException, ValidationException, RateLimitException, or ApiException; network failures throw TransportException. Each exposes statusCode, responseData, responseHeaders, method, and uri.
Validation error bags returned by the API are preserved in
ValidationException::$responseData['errors']:
use Payinto\Exception\ValidationException; try { // amount_control is required for dynamic virtual-account creation. $payinto->virtualAccounts->createDynamic([ 'amount' => 150000, 'merchant_reference' => 'ORDER-1', ]); } catch (ValidationException $exception) { // Read the API message: "One or more of the given data has an error." $message = $exception->getMessage(); // Read field-level errors, for example: ["The amount control field is required."] $amountControlErrors = $exception->responseData['errors']['amount_control'] ?? []; }
If an API error envelope is returned with a successful HTTP status instead of
a non-2xx status, the SDK returns it as an ApiResponse; inspect
$response->status() and $response->raw() in that case.
Business API
All Business API calls use the configured secret key.
// Check that the Business API is available. $payinto->business->status(); // Resolve the business and environment associated with the secret key. $payinto->business->whoami(); // Query the status of a Business API transaction by reference. $payinto->transactions->status('merchant-reference'); // List accounts for the business. $payinto->accounts->list([ 'page' => 1, 'per_page' => 25, ]); // Retrieve the business's main account. $payinto->accounts->main(); // Retrieve transactions belonging to the main account. $payinto->accounts->mainTransactions([ 'page' => 1, 'per_page' => 50, ]); // Retrieve a specific account. $payinto->accounts->get($accountId); // Retrieve transactions belonging to a specific account. $payinto->accounts->transactions($accountId); // Create an individual customer. $customer = $payinto->customers->createIndividual([ 'first_name' => 'Ada', 'last_name' => 'Example', 'gender' => 'female', 'date_of_birth' => '1990-01-15', 'email' => 'ada@example.com', 'phone_number' => '+2348000000000', 'country_code' => 'NG', 'bvn' => '12345678901', 'nin' => '12345678901', // optional 'address' => [ 'address_line1' => '1 Example Street', 'city' => 'Lagos', 'state' => 'Lagos', 'country' => 'NG', ], ]); // Search or list customers. $payinto->customers->list([ 'search' => 'Ada', 'page' => 1, 'per_page' => 25, ]); // Retrieve a customer by ID. $payinto->customers->get($customer->data()['id']); // Update permitted customer fields. All update fields are optional, so send // only the values that need to change. // // Important: once a customer's identity has been verified, identity/profile // data and identification information cannot be changed. This includes // names, gender, date of birth, BVN, NIN, and other verified identity data. // Make those corrections before identity verification, where permitted. $updatedCustomer = $payinto->customers->update($customer->data()['id'], [ // Non-identity contact fields may be updated when allowed by the API. 'email' => 'ada.updated@example.com', 'phone_number' => '+2348000000000', 'country_code' => 'NG', 'metadata' => [ 'source' => 'web', ], ]); // Create a business customer. $payinto->customers->createBusiness([ 'business_name' => 'Example Ltd', 'business_registration_type' => 'limited', 'business_registration_number' => 'RC123456', 'business_registration_date' => '2020-01-15', 'first_name' => 'Ada', 'last_name' => 'Example', 'gender' => 'female', 'date_of_birth' => '1990-01-15', 'email' => 'ada@example.com', 'phone_number' => '+2348000000000', 'country_code' => 'NG', 'bvn' => '12345678901', 'nin' => '12345678901', // optional 'address' => [ 'address_line1' => '1 Example Street', 'city' => 'Lagos', 'state' => 'Lagos', 'country' => 'NG', ], ]); // Upload a customer photo as a multipart request. $payinto->customers->uploadPhoto($customer->data()['id'], file_get_contents('photo.jpg'), 'photo.jpg'); // Verify customer identity without an OTP flow. $payinto->customers->verifyIdentity($customer->data()['id'], [ 'type' => 'BVN', // use BVN or NIN ]); // Start an OTP-based customer identity verification. $identityChallenge = $payinto->customers->initiateIdentity($customer->data()['id'], [ 'type' => 'BVN', // use BVN or NIN ]); // Complete an OTP-based customer identity verification. $payinto->customers->validateIdentity($customer->data()['id'], [ 'type' => 'BVN', 'reference' => $identityChallenge->data()['reference'], 'otp' => '123456', ]); // List virtual accounts. $payinto->virtualAccounts->list(); // Create a reusable static virtual account for an existing customer. // customer_id and merchant_reference are required. The main business // account is used for settlement unless settlement_account_id is provided. $staticVirtualAccount = $payinto->virtualAccounts->createStatic([ 'customer_id' => $customer->data()['id'], 'merchant_reference' => 'CUSTOMER-VA-001', // 'settlement_account_id' => 'account-uuid', // optional 'metadata' => [ 'source' => 'customer-onboarding', ], ]); // Create a time-limited dynamic virtual account. // amount is an integer in the API's minor monetary unit, and amount_control // must be Fixed, UnderPayment, or OverPayment. $dynamicVirtualAccount = $payinto->virtualAccounts->createDynamic([ 'amount' => 150000, 'amount_control' => 'Fixed', 'valid_for' => 3600, // optional: lifetime in seconds (15 minutes to 24 hours) 'merchant_reference' => 'ORDER-1', ]); // Retrieve a virtual account. $payinto->virtualAccounts->get($accountId); // List transactions for a virtual account. $payinto->virtualAccounts->transactions($accountId); // Retrieve a virtual-account transaction by ID. $payinto->virtualAccounts->transaction($transactionId); // Retrieve a virtual-account transaction by reference. $payinto->virtualAccounts->transactionByReference('ORDER-1'); // List supported banks. $payinto->transfers->banks(); // Resolve a beneficiary bank account before transferring funds. $resolvedBeneficiary = $payinto->transfers->resolveAccount([ 'bank_code' => '058', 'account_number' => '0123456789', ]); // Calculate the fee for a transfer. $payinto->transfers->fee([ 'amount' => 100000, 'currency' => 'NGN', // optional; defaults to the debit account currency 'beneficiary_bank_code' => '058', // optional ]); // Initiate a bank transfer after resolving the beneficiary account. // resolved_account_reference must come from transfers->resolveAccount(). // amount is an integer in the API's minor monetary unit. $transfer = $payinto->transfers->initiate([ 'resolved_account_reference' => $resolvedBeneficiary->data()['reference'], 'beneficiary_bank_code' => '058', 'beneficiary_account_number' => '0123456789', 'amount' => 100000, 'merchant_reference' => 'TRANSFER-1', 'narration' => 'Payment for order ORDER-1', // optional 'save_beneficiary' => false, // optional // 'debit_account_id' => 'account-uuid', // optional; defaults to main account ]); // List available value-added services. $payinto->vas->services(); // Retrieve a value-added service. $payinto->vas->service($serviceId); // List billers for a value-added service. $payinto->vas->serviceBillers($serviceId); // Retrieve a biller. $payinto->vas->biller($billerId); // List products offered by a biller. $payinto->vas->billerProducts($billerId); // Retrieve a value-added service product. $payinto->vas->product($productId); // Use the IDs returned by the VAS lookup calls above in the verification and // payment payloads below. Replace these placeholders with real UUIDs. // Verify a cable TV account before payment. $cableCustomer = $payinto->vas->verifyCableTv([ 'product_id' => $cableProductId, 'smart_card_number' => '1234567890', ]); // Verify an electricity meter before payment. $electricityCustomer = $payinto->vas->verifyElectricity([ 'product_id' => $electricityProductId, 'meter_number' => '1234567890123', ]); // Purchase airtime. $payinto->vas->payAirtime([ 'biller_id' => $airtimeBillerId, 'product_id' => $airtimeProductId, 'country_code' => 'NG', 'phone_number' => '+2348000000000', 'amount' => 1000, 'merchant_reference' => 'AIRTIME-ORDER-1', // optional but recommended // 'debit_account_id' => 'account-uuid', // optional; defaults to main account ]); // Purchase mobile data. $payinto->vas->payData([ 'biller_id' => $dataBillerId, 'product_id' => $dataProductId, 'country_code' => 'NG', 'phone_number' => '+2348000000000', 'amount' => 5000, 'merchant_reference' => 'DATA-ORDER-1', // optional but recommended ]); // Pay a cable TV bill. $payinto->vas->payCableTv([ 'biller_id' => $cableBillerId, 'product_id' => $cableProductId, 'smart_card_number' => '1234567890', 'amount' => 5000, 'merchant_reference' => 'CABLE-ORDER-1', // optional but recommended ]); // Pay an electricity bill. $payinto->vas->payElectricity([ 'biller_id' => $electricityBillerId, 'product_id' => $electricityProductId, 'meter_number' => '1234567890123', 'amount' => 50000, 'merchant_reference' => 'ELECTRICITY-ORDER-1', // optional but recommended ]); // Pay a betting account. $payinto->vas->payBetting([ 'biller_id' => $bettingBillerId, 'product_id' => $bettingProductId, 'customer_id' => '123456789', 'amount' => 5000, 'merchant_reference' => 'BETTING-ORDER-1', // optional but recommended ]); // Create an OTP challenge. $otp = $payinto->addons->createOtp([ 'sender' => 'Payinto', 'length' => 6, 'expiry' => 10, // minutes accepted by the API: 5 to 60 'channel' => ['sms'], // sms, whatsapp, email, or a combination 'recipient' => [ 'phone_number' => '+2348000000000', 'phone_number_country_code' => 'NG', ], ]); // Validate an OTP challenge. $payinto->addons->validateOtp($otp->data()['reference'], [ 'reference' => $otp->data()['reference'], 'otp' => '123456', ]); // Run a customer credit check. $payinto->addons->creditCheck([ 'type' => 'individual', 'bvn' => '12345678901', 'credit_bureau' => 'CRC', ]); // Calculate the credit-check fee. $payinto->addons->creditCheckFee([ 'type' => 'individual', 'credit_bureau' => 'CRC', ]); // Verify a Bank Verification Number. $payinto->addons->bvnVerification([ 'bvn' => '12345678901', ]); // Retrieve the BVN verification fee. $payinto->addons->bvnVerificationFee();
Flexible endpoint payloads are passed as associative arrays. Query parameters are passed as the final array argument where supported.
Checkout API
This PHP SDK provides server-side access to the Payinto Checkout API. For the browser-based popup checkout experience, use the official @payinto/checkout-sdk npm package instead. The popup SDK is responsible for opening and managing the customer-facing checkout flow, while this package is intended for backend initialization, reconciliation, payment status checks, and related server-side operations.
Initialize Checkout
// Initialize a server-side Checkout session. $initialized = $payinto->checkout->initialize([ 'amount' => 150000, 'currency' => 'NGN', 'merchant_reference' => 'ORDER-1', 'customer' => [ 'name' => 'Ada Example', // optional 'email' => 'ada@example.com', // required 'phone_number' => '+2348000000000', // optional ], 'redirect_url' => 'https://example.com/checkout/complete', // optional 'metadata' => [ 'order_id' => 'ORDER-1', ], // optional // 'settlement_account_id' => 'account-uuid', // optional ]);
The returned checkout token is consumed by the browser popup integration. Use the official @payinto/checkout-sdk package for retrieving the checkout session, submitting payment methods, and logging customer-facing checkout activity.
Checkout Transaction Status Query (TSQ)
// Query the final status of a Checkout payment from the server. $result = $payinto->checkout->transactionStatus('ORDER-1'); if ($result->data()['payment_status'] === 'successful') { /* fulfil order */ }
Webhook verification
Verify the webhook before processing its JSON payload. Always pass the exact raw request body because Payinto signs the original bytes before JSON decoding or re-encoding.
Laravel
use Illuminate\Http\Request; use Payinto\Client; public function webhook(Request $request, Client $payinto): array { // Verify the request before trusting or processing its payload. $valid = $payinto->webhooks->verifyRequest($request); abort_unless($valid, 401, 'Invalid webhook signature.'); return $request->json()->all(); }
Non-Laravel PHP
For a plain PHP application, read the raw request body and the X-Payinto-Signature HTTP header before decoding the JSON payload:
use Payinto\Client; $payinto = new Client([ 'secretKey' => $_ENV['PAYINTO_LIVE_SECRET_KEY'], 'webhookSecret' => $_ENV['PAYINTO_LIVE_WEBHOOK_SECRET'], 'webhookTimestampTolerance' => 300, ]); $rawBody = (string) file_get_contents('php://input'); $signatureHeader = $_SERVER['HTTP_X_PAYINTO_SIGNATURE'] ?? ''; // Verify the raw body and Payinto signature before decoding JSON. if (! $payinto->webhooks->verify($rawBody, $signatureHeader)) { http_response_code(401); exit('Invalid webhook signature.'); } $payload = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
The package automatically handles the X-Payinto-Signature format: t={timestamp},v1={signature}. The verifier computes an HMAC-SHA256 signature over {timestamp}.{raw_request_body} and compares it using a timing-safe comparison. Webhook secrets must remain server-side and must never be exposed to browser code.
Set webhookTimestampTolerance to the number of accepted seconds. For example, 300 allows a five-minute clock difference, while 0 disables timestamp checking but keeps HMAC verification enabled.
Custom requests
Future endpoints can be accessed through the raw client while retaining the same response and exception behavior:
// Call an endpoint that does not yet have a dedicated SDK method. $payinto->request('GET', 'business/new-endpoint', ['query'=>['page'=>1]]);
Testing
composer install
composer test
Use a mocked Guzzle client in application tests; never commit API keys. See tests/Unit/ClientTest.php for request and authentication examples.