payintohq/payinto-php-sdk

PHP SDK for the Payinto Business and Checkout APIs.

Maintainers

Package info

github.com/PayintoHQ/payinto-php-sdk

pkg:composer/payintohq/payinto-php-sdk

Transparency log

Statistics

Installs: 55

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.1 2026-08-04 22:10 UTC

This package is auto-updated.

Last update: 2026-08-04 22:11:09 UTC


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.