Checkout SDK
Add a secure Payinto payment popup to your website with a few lines of code.
The Checkout SDK opens a Payinto payment popup on your page. Your customer pays by card or bank transfer, and you never handle card details. It runs in the browser with your public key.
The SDK is on npm as @payinto/checkout-sdk, and you can also load it with a script tag.
How it works
Your page opens the popup with the order amount and your merchant_reference.
Your customer pays. The SDK calls onSuccess so you can update the page.
Payinto sends your server the signed checkout.payment.succeeded webhook. Fulfill the order from here, not from the browser.
Before you start
- Ask Payinto to enable the
checkoutmodule for your business. Without it, Checkout returns403. - Copy your public key from Settings > API Keys & Webhooks in the dashboard. Use
pk_test_while you build. - Register a webhook in the same environment.
Open the popup
Amounts are in the lowest currency unit, so 150000 is ₦1,500.00. Open the popup from a click or a similar user action.
npm install @payinto/checkout-sdkimport PayintoCheckout from '@payinto/checkout-sdk';
document.querySelector('#pay-button').addEventListener('click', () => {
PayintoCheckout.open({
mode: 'public_key',
public_key: 'pk_test_your_public_key',
amount: 150000,
currency: 'NGN',
merchant_reference: 'ORDER-10001',
customer: {
email: '[email protected]',
name: 'Ada Example',
},
onSuccess: (payment) => {
// Update the page. Your server fulfills the order from the webhook.
window.location.href = `/orders/10001?reference=${payment.reference}`;
},
onError: (error) => {
console.error(error.code, error.message);
},
onClose: () => {
console.log('Checkout closed');
},
});
});Never put your secret key (sk_) in browser code. Only the public key (pk_) belongs here.
Options
| Option | Required | Description |
|---|---|---|
mode | Yes | public_key, or token for a server-created session |
public_key | Yes | Your pk_test_ or pk_live_ key |
amount | Yes | The order amount in the lowest currency unit |
customer.email | Yes | The customer's email address |
customer.name, customer.phone_number | No | More customer details |
currency | No | Defaults to NGN |
merchant_reference | No, but recommended | Your order reference, up to 128 characters |
metadata | No | Your own data, returned in the webhook |
settlement_account_id | No | The account that receives funds. Defaults to your main account |
redirect_url | No | A URL to send the customer to after payment |
onSuccess, onError, onClose | No | Browser callbacks |
Call PayintoCheckout.close() to close the popup from your own code.
Browser callbacks
onSuccess receives:
{
type: 'checkout.succeeded',
status: 'success',
reference: 'PIF01EXAMPLE',
merchant_reference: 'ORDER-10001',
token: 'checkout_token'
}onError receives:
{
type: 'checkout.error',
code: 'initialization_failed',
message: 'Unable to connect to checkout. Please check your connection and try again.'
}Callbacks don't prove payment
Browser callbacks and redirects can be faked or lost, for example if the customer closes the tab. Only use them to update the page.
Confirm the payment on your server
With the webhook
Subscribe to checkout.payment.succeeded. It's sent for both card and bank transfer payments, and data.payment_method says which.
{
"event": "checkout.payment.succeeded",
"id": "01J1234567890ABCDEFGHJKMNP",
"created_at": "2026-10-31T09:00:00.000000Z",
"api_version": "2026-05-27",
"data": {
"id": "0196431e-1b44-7d10-a2c9-8e5f7b1c2d3e",
"reference": "PIF01EXAMPLE",
"merchant_reference": "ORDER-10001",
"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-31T09:00:00.000000Z",
"customer": { "email": "[email protected]" },
"metadata": { "order_id": "10001" }
}
}Before you fulfill the order:
- Verify the signature.
- Find the order with
merchant_reference. - Check
currency, and check thatbase_amountmatches your order total. - Process each payment once, using
data.idordata.referenceas a unique key, because deliveries can repeat.
Fulfill from this event only. A bank transfer checkout also sends virtual_account.transfer.received and transaction.credit.succeeded for the same payment. Pending, failed, expired, and refunded checkout events aren't sent.
The verification guide has examples for PHP, JavaScript, and Python. In Laravel, the PHP SDK can do it for you:
<?php
use Illuminate\Http\Request;
use Payinto\Client;
use Payinto\Webhooks\WebhookEvents;
Route::post('/webhooks/payinto', function (Request $request, Client $payinto) {
abort_unless($payinto->webhooks->verifyRequest($request), 401);
$event = $request->json()->all();
if ($event['event'] === WebhookEvents::CHECKOUT_PAYMENT_SUCCEEDED) {
// Queue fulfillment for $event['data']['merchant_reference'].
}
return response()->noContent();
});With a status check
If a webhook is late, ask Payinto directly from your server with your secret key. You can use the Payinto reference or your merchant_reference.
curl https://api.payinto.co/api/v1/checkout/tsq/ORDER-10001 \
--header "Authorization: Bearer $PAYINTO_SECRET_KEY" \
--header "Accept: application/json"data.payment_status is pending, successful, failed, cancelled, or expired. A 404 means no checkout matches that reference for this business and environment.
Understand the amounts
| Field | Meaning |
|---|---|
base_amount | Your order amount |
customer_charge_amount | Any charge added to what the customer pays. customer_fee_amount is the same value |
amount | What the customer paid |
fee, vat, stamp_duty, total_fee | What Payinto deducted |
settled_amount | What you received |
You can choose in the dashboard's developer settings to pass charges on to your customers. When you do, charges_applied is true and amount is higher than base_amount.
Create the session on your server
By default, the SDK creates the checkout session from the browser. To keep the amount and reference out of the browser, or to reuse the session, create it on your server and open it with mode: 'token'.
This request also uses your public key. Secret keys aren't accepted here.
curl https://api.payinto.co/api/v1/checkout/initialize \
--header "Authorization: Bearer $PAYINTO_PUBLIC_KEY" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data '{
"amount": 150000,
"currency": "NGN",
"merchant_reference": "ORDER-10001",
"customer": { "email": "[email protected]", "name": "Ada Example" }
}'Send token and checkout_url to your page, then open the popup:
PayintoCheckout.open({
mode: 'token',
token: session.token,
checkout_url: session.checkout_url,
merchant_reference: 'ORDER-10001',
onSuccess: (payment) => console.log(payment.reference),
});Sessions expire after 60 minutes. During that time, repeating a request with the same merchant_reference and payload returns the same session with the X-Idempotency-Replay: true header. Reusing the reference with a different payload returns 409.
Test in the sandbox
Use your pk_test_ key and sk_test_ key. To complete a bank transfer payment, simulate a deposit from API Keys & Webhooks in the dashboard's test environment. Your test webhook then receives checkout.payment.succeeded.
Before you go live, switch to your pk_live_ key, sk_live_ key, and live webhook secret.
Troubleshooting
| Problem | Likely cause |
|---|---|
Checkout API access is not enabled | The checkout module isn't enabled for your business |
Invalid API Key | The key is wrong, or you used a secret key where a public key is needed |
| The popup doesn't load on your site | Your Content Security Policy must allow https://checkout.payinto.co as a frame and https://api.payinto.co as a connection |
onSuccess ran, but no webhook arrived | Check the webhook logs in the dashboard, then use the status check |
| Duplicate orders | Process each payment once with data.id, and fulfill only from checkout.payment.succeeded |