PayintoDocs

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 checkout module for your business. Without it, Checkout returns 403.
  • 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-sdk
import 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: 'customer@example.com',
      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

OptionRequiredDescription
modeYespublic_key, or token for a server-created session
public_keyYesYour pk_test_ or pk_live_ key
amountYesThe order amount in the lowest currency unit
customer.emailYesThe customer's email address
customer.name, customer.phone_numberNoMore customer details
currencyNoDefaults to NGN
merchant_referenceNo, but recommendedYour order reference, up to 128 characters
metadataNoYour own data, returned in the webhook
settlement_account_idNoThe account that receives funds. Defaults to your main account
redirect_urlNoA URL to send the customer to after payment
onSuccess, onError, onCloseNoBrowser 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": "customer@example.com" },
    "metadata": { "order_id": "10001" }
  }
}

Before you fulfill the order:

  1. Verify the signature.
  2. Find the order with merchant_reference.
  3. Check currency, and check that base_amount matches your order total.
  4. Process each payment once, using data.id or data.reference as 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

FieldMeaning
base_amountYour order amount
customer_charge_amountAny charge added to what the customer pays. customer_fee_amount is the same value
amountWhat the customer paid
fee, vat, stamp_duty, total_feeWhat Payinto deducted
settled_amountWhat 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": "customer@example.com", "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

ProblemLikely cause
Checkout API access is not enabledThe checkout module isn't enabled for your business
Invalid API KeyThe key is wrong, or you used a secret key where a public key is needed
The popup doesn't load on your siteYour 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 arrivedCheck the webhook logs in the dashboard, then use the status check
Duplicate ordersProcess each payment once with data.id, and fulfill only from checkout.payment.succeeded

On this page