PayPay
Accept payments from 60+ million PayPay users in Japan, the country's dominant QR code payment platform with the highest adoption rate among Japanese mobile payment solutions.
Overview
User numbers are approximate and based on publicly available information. Actual active user counts may vary.
PayPay is Japan's largest cashless payment service with over 60 million users (nearly half of Japan's population). Launched in 2018 as a joint venture between SoftBank and Yahoo Japan, PayPay quickly became the market leader through aggressive cashback campaigns and widespread merchant adoption, making it essential for any business targeting Japanese consumers.
Key Features:
- ✅ 60+ million users - one of Japan's leading payment apps
- ✅ 4.4+ million locations - Accepted nationwide
- ✅ No fees for customers - Free to use for payments
- ✅ Instant confirmation - Real-time payment processing
- ✅ Rewards program - PayPay Bonus points on purchases
- ✅ Multiple funding sources - Bank, credit card, convenience store
Supported Regions
| Region | Currency | Min Amount | Max Amount | API Version |
|---|---|---|---|---|
| Japan | JPY | ¥100 | ¥1,000,000 | 2017-11-02 |
How It Works
Customer Experience:
- Customer selects "PayPay" at checkout
- Redirected to PayPay payment page
- Opens PayPay app automatically (on mobile)
- Reviews transaction details
- Authenticates if required (PIN or biometric)
- Confirms payment with one tap
- Earns PayPay Bonus points
- Returns to merchant website
Typical completion time: 30-90 seconds
Implementation
Step 1: Create PayPay Source
- cURL
- Node.js
- PHP
- Python
- Ruby
- Go
- Java
- C#
curl https://api.omise.co/sources \
-u skey_test_YOUR_SECRET_KEY: \
-d "type=paypay" \
-d "amount=100000" \
-d "currency=JPY"
const omise = require('omise')({
secretKey: 'skey_test_YOUR_SECRET_KEY'
});
const source = await omise.sources.create({
type: 'paypay',
amount: 100000, // ¥100,000
currency: 'JPY'
});
<?php
$source = OmiseSource::create(array(
'type' => 'paypay',
'amount' => 100000,
'currency' => 'JPY'
));
?>
import omise
omise.api_secret = 'skey_test_YOUR_SECRET_KEY'
source = omise.Source.create(
type='paypay',
amount=100000,
currency='JPY'
)
require 'omise'
Omise.api_key = 'skey_test_YOUR_SECRET_KEY'
source = Omise::Source.create({
type: 'paypay',
amount: 100000,
currency: 'JPY'
})
source, err := client.Sources().Create(&operations.CreateSource{
Type: "paypay",
Amount: 100000,
Currency: "JPY",
})
Source source = client.sources().create(new Source.CreateParams()
.type("paypay")
.amount(100000L)
.currency("JPY"));
var source = await client.Sources.Create(new CreateSourceRequest
{
Type = "paypay",
Amount = 100000,
Currency = "JPY"
});
Response:
{
"object": "source",
"id": "src_test_5rt6s9vah5lkvi1rh9c",
"type": "paypay",
"flow": "redirect",
"amount": 100000,
"currency": "JPY"
}
Step 2: Create Charge
curl https://api.omise.co/charges \
-u skey_test_YOUR_SECRET_KEY: \
-d "amount=100000" \
-d "currency=JPY" \
-d "source=src_test_5rt6s9vah5lkvi1rh9c" \
-d "return_uri=https://yourdomain.com/payment/callback"
Step 3: Redirect Customer
app.post('/checkout/paypay', async (req, res) => {
try {
const { amount, order_id, customer_email } = req.body;
// Validate amount (¥100 - ¥500,000)
if (amount < 100 || amount > 500000) {
return res.status(400).json({
error: 'Amount must be between ¥100 and ¥500,000'
});
}
// Create source
const source = await omise.sources.create({
type: 'paypay',
amount: amount,
currency: 'JPY'
});
// Create charge
const charge = await omise.charges.create({
amount: amount,
currency: 'JPY',
source: source.id,
return_uri: `${process.env.BASE_URL}/payment/callback`,
metadata: {
order_id: order_id,
customer_email: customer_email
}
});
// Redirect to PayPay
res.redirect(charge.authorize_uri);
} catch (error) {
console.error('PayPay error:', error);
res.status(500).json({ error: error.message });
}
});
Step 4: Handle Return
app.get('/payment/callback', async (req, res) => {
try {
const chargeId = req.query.charge_id;
const charge = await omise.charges.retrieve(chargeId);
if (charge.status === 'successful') {
await processOrder(charge.metadata.order_id);
res.redirect('/payment-success');
} else if (charge.status === 'failed') {
res.redirect('/payment-failed?reason=' + charge.failure_message);
} else {
res.redirect('/payment-pending');
}
} catch (error) {
res.redirect('/payment-error');
}
});
Step 5: Handle Webhook
app.post('/webhooks/omise', (req, res) => {
const event = req.body;
if (event.key === 'charge.complete' && event.data.source.type === 'paypay') {
const charge = event.data;
if (charge.status === 'successful') {
processOrder(charge.metadata.order_id);
sendConfirmationEmail(charge.metadata.customer_email);
// Log successful payment
console.log(`PayPay payment successful: ${charge.id}`);
} else if (charge.status === 'failed') {
handleFailedPayment(charge.metadata.order_id);
}
}
res.sendStatus(200);
});
Complete Implementation Example
// Express.js server
const express = require('express');
const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY
});
const app = express();
app.use(express.json());
app.post('/checkout/paypay', async (req, res) => {
try {
const { amount, order_id, customer_email, customer_name } = req.body;
// Validate amount (¥100 - ¥500,000)
if (amount < 100 || amount > 500000) {
return res.status(400).json({
error: 'Amount must be between ¥100 and ¥500,000'
});
}
// Calculate estimated PayPay Bonus points (example: 0.5%)
const estimatedBonus = Math.floor(amount * 0.005);
// Create source
const source = await omise.sources.create({
type: 'paypay',
amount: amount,
currency: 'JPY'
});
// Create charge
const charge = await omise.charges.create({
amount: amount,
currency: 'JPY',
source: source.id,
return_uri: `${process.env.BASE_URL}/payment/callback`,
metadata: {
order_id: order_id,
customer_email: customer_email,
customer_name: customer_name,
payment_method: 'paypay',
estimated_bonus: estimatedBonus
}
});
// Return authorization URL
res.json({
authorize_uri: charge.authorize_uri,
charge_id: charge.id,
estimated_bonus: estimatedBonus
});
} catch (error) {
console.error('PayPay error:', error);
res.status(500).json({ error: error.message });
}
});
// Callback handler
app.get('/payment/callback', async (req, res) => {
try {
const chargeId = req.query.charge_id;
const charge = await omise.charges.retrieve(chargeId);
if (charge.status === 'successful') {
const bonus = charge.metadata.estimated_bonus || 0;
res.redirect(`/order-success?order=${charge.metadata.order_id}&bonus=${bonus}`);
} else {
res.redirect(`/payment-failed?charge=${chargeId}`);
}
} catch (error) {
res.redirect('/payment-error');
}
});
// Webhook handler
app.post('/webhooks/omise', (req, res) => {
const event = req.body;
if (event.key === 'charge.complete') {
const charge = event.data;
if (charge.source.type === 'paypay') {
if (charge.status === 'successful') {
updateOrderStatus(charge.metadata.order_id, 'paid');
sendConfirmation(charge.metadata.customer_email, {
amount: charge.amount,
bonus: charge.metadata.estimated_bonus
});
console.log(`PayPay payment successful: ${charge.id}`);
} else {
updateOrderStatus(charge.metadata.order_id, 'failed');
console.log(`PayPay payment failed: ${charge.id}`);
}
}
}
res.sendStatus(200);
});
// Helper functions
async function updateOrderStatus(orderId, status) {
await db.orders.update({ id: orderId }, { status: status });
}
async function sendConfirmation(email, details) {
// Send email confirmation with PayPay Bonus info
}
app.listen(3000);
Refund Support
PayPay supports full and partial refunds within 60 days:
// Full refund
const fullRefund = await omise.charges.refund('chrg_test_...', {
amount: 100000
});
// Partial refund
const partialRefund = await omise.charges.refund('chrg_test_...', {
amount: 50000 // Half refund
});
- Refunds are processed within 1-2 business days
- Customers receive refunds in their PayPay balance
- PayPay Bonus points are automatically adjusted
- Supported within 60 days of original transaction
Refund windows and policies are subject to change. Always verify current refund capabilities via the Omise API documentation or your merchant dashboard.
Common Issues & Troubleshooting
Issue: PayPay app not installed
Cause: Customer doesn't have PayPay app installed
Solution:
function checkPayPayApp() {
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
if (isMobile) {
return `
<div class="paypay-app-check">
<p>PayPay app is required for payment.</p>
<div class="download-links">
<a href="https://play.google.com/store/apps/details?id=jp.ne.paypay.android.app">
<img src="/images/google-play-badge-jp.png" alt="Google Playで入手">
</a>
<a href="https://apps.apple.com/jp/app/paypay/id1435783608">
<img src="/images/app-store-badge-jp.png" alt="App Storeからダウンロード">
</a>
</div>
</div>
`;
}
}
Issue: Insufficient balance
Cause: Customer's PayPay balance is too low and no backup payment method
Solution:
<div class="paypay-balance-info">
<h3>PayPayの残高が不足しています</h3>
<p>以下の方法でチャージしてください:</p>
<ul class="charge-methods">
<li>🏦 <strong>銀行口座</strong> - リアルタイムチャージ</li>
<li>🏧 <strong>セブン銀行ATM</strong> - 現金チャージ</li>
<li>🏪 <strong>ローソン</strong> - レジでチャージ</li>
<li>💳 <strong>クレジットカード</strong> - 登録して自動チャージ</li>
<li>👥 <strong>PayPay残高の送受信</strong> - 友達から受け取る</li>
</ul>
<p class="amount-needed">必要金額: <strong>¥{{amount}}</strong></p>
</div>
Issue: Daily limit exceeded
Cause: Customer reached their daily or monthly limit
Solution:
function handlePayPayLimitExceeded() {
return {
error: '利用限度額を超過しました',
message: 'PayPayの1日または1か月の利用限度額に達しました。',
solutions: [
{
title: '本人確認を完了する',
description: '本人確認済みアカウントは¥500,000/日まで利用可能',
steps: [
'PayPayアプリを開く',
'アカウント > 本人確認',
'身分証明書をアップロード',
'審査完了まで数時間待つ'
]
},
{
title: '別の支払い方法を使用',
description: 'クレジットカードや他の決済方法をご利用ください'
},
{
title: '明日再試行',
description: '利用限度額は毎日0時にリセットされます'
}
]
};
}
Issue: Payment authentication failed
Cause: Customer failed PIN or biometric authentication
Solution:
function handleAuthenticationError() {
return `
<div class="auth-error">
<h3>認証に失敗しました</h3>
<p>以下をお試しください:</p>
<ul>
<li>PINコードを再入力してください</li>
<li>生体認証を再試行してください</li>
<li>PayPayアプリを再起動してください</li>
<li>PINをお忘れの場合は、PayPayアプリで再設定してください</li>
</ul>
<button onclick="retryPayment()">再試行</button>
</div>
`;
}
Issue: Payment timeout
Cause: Customer didn't complete payment within time limit
Solution:
// 5-minute payment window
const PAYMENT_TIMEOUT = 5 * 60 * 1000;
setTimeout(() => {
if (!paymentConfirmed) {
showMessage('お支払いの有効期限が切れました。もう一度やり直してください。');
enableRetryButton();
}
}, PAYMENT_TIMEOUT);
Best Practices
1. Display PayPay Benefits (Japanese UI)
<div class="paypay-benefits">
<img src="/images/paypay-logo.svg" alt="PayPay" height="40">
<h3>PayPayで支払う</h3>
<ul class="benefits-list">
<li>✓ 即時決済確認</li>
<li>✓ PayPayボーナスポイント獲得</li>
<li>✓ 安全なPIN/生体認証</li>
<li>✓ 手数料無料</li>
<li>✓ 日本全国440万店舗以上で利用可能</li>
</ul>
<p class="bonus-note">
🎁 この購入で<strong>約{{points}}円相当</strong>のPayPayボーナスを獲得
</p>
</div>
2. Provide Bilingual Support
function getPayPayInstructions(language = 'ja') {
const instructions = {
'ja': {
title: 'PayPayでのお支払い方法',
steps: [
'「PayPayで支払う」をクリック',
'PayPayアプリが自動的に開きます',
'取引内容を確認',
'PINまたは生体認証で認証',
'お支払いを確定'
]
},
'en': {
title: 'How to pay with PayPay',
steps: [
'Click "Pay with PayPay"',
'PayPay app will open automatically',
'Review transaction details',
'Authenticate with PIN or biometric',
'Confirm payment'
]
}
};
return instructions[language] || instructions['ja'];
}