Search by

payintohq / payinto-php-sdk

victorybiz

PHP SDK for the Payinto Business and Checkout APIs.

Package info

github.com/PayintoHQ/payinto-php-sdk

pkg:composer/payintohq/payinto-php-sdk

Statistics

Installs: 243

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.3.0 2026-10-23 14:32 UTC

This package is auto-updated.

Last update: 2026-10-23 14:32:56 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 resolveAccount() or resolveRecipient().
// 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') {
    $merchantReference = $result->data()['merchant_reference'];
    // Match the order, currency, and expected amount; fulfill once.
}

Checkout TSQ returns data.reference, nullable data.merchant_reference, payment_status, currency, the amount breakdown, nullable paid_at, and nullable transaction_id. The envelope status is success; the payment state is successful.

For account transactions, use $payinto->transactions->status($reference) and inspect data.status. That response also includes nullable data.merchant_reference, but currently reads it only from transaction metadata. A transaction webhook can therefore contain a reference resolved from its linked source while TSQ returns null. Checkout TSQ reads the payment inflow field with a fallback to cached checkout data.

Webhook events

Event Trigger
checkout.payment.succeeded Verified bank-transfer or card checkout payment succeeds. Use this event for checkout fulfillment.
virtual_account.transfer.received A virtual-account deposit is credited, including a bank-transfer checkout.
transaction.credit.succeeded A credit is recorded through the account-credit flow.
transaction.debit.succeeded A debit is recorded through the account-debit flow. External transfer or bill-payment completion is not implied.

All events use the envelope fields event, id, created_at, api_version, and data. All four expose nullable data.merchant_reference. Transaction events resolve it from their linked payment inflow, transfer, or VAS record, then source metadata and transaction metadata, including legacy checkout metadata. reference remains the Payinto reference.

A checkout bank transfer can emit checkout success, virtual-account deposit, and transaction credit events for one payment. Fulfill only from checkout.payment.succeeded, with an idempotent payment key. Webhook retries and replays are possible. Checkout pending, failed, expired, and refunded events are not currently emitted. Future payment methods such as USSD must connect their verified confirmations to the unified event service.

use Payinto\Webhooks\WebhookEvents;

// Run only after verifying the signature on the exact raw body.
if ($payload['event'] === WebhookEvents::CHECKOUT_PAYMENT_SUCCEEDED) {
    $payment = $payload['data'];
    $merchantReference = $payment['merchant_reference'];
    // Match the order, currency, and amount; persist or enqueue durably.
    // Enforce uniqueness on data.id or data.reference before fulfillment.
}

The checkout event data has this shape (illustrative identifiers):

{
  "id": "payment_inflow_001",
  "reference": "PIF-1",
  "merchant_reference": "ORDER-1",
  "payment_method": "bank_transfer",
  "currency": "NGN",
  "base_amount": 150000,
  "customer_charge_amount": 2000,
  "customer_fee_amount": 2000,
  "amount": 152000,
  "charges_applied": true,
  "fee": 2000,
  "vat": 0,
  "stamp_duty": 0,
  "total_fee": 2000,
  "settled_amount": 150000,
  "payment_status": "successful",
  "paid_at": "2026-10-22T12:00:00.000000Z",
  "customer": null,
  "metadata": null
}

For cards, payment_method is card. All amounts use minor currency units. Match the customer's expected payment against amount; base_amount is the order amount and settled_amount is after fees. customer and metadata preserve checkout input or are null. The browser SDK's checkout.succeeded callback is a UI notification; keep fulfillment on your server using a verified webhook or authenticated checkout TSQ.

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.

Transfers to Payinto Tags

Resolve the tag, confirm the returned recipient, then initiate a transfer using the resolved bank details. The transfers module must be enabled for your API key. Amounts use minor units. Use the same optional debit_account_id throughout. Sandbox keys resolve only sandbox recipient records and simulate bank resolution.

$recipient = $payinto->transfers->resolveRecipient([
    'recipient' => '@tag_recipient',
]);
// Confirm the recipient details before initiating the transfer.
$data = $recipient->data();
$payinto->transfers->initiate([
    'resolved_account_reference' => $data['resolved_account_reference'],
    'beneficiary_bank_code' => $data['bank_code'],
    'beneficiary_account_number' => $data['account_number'],
    'amount' => 250000,
    'merchant_reference' => 'tag-payout-001',
]);