Complete, production-ready TatraPay+ (Tatra banka) payment gateway integration for Next.js applications.
- Card Payments (CARD_PAY) - Visa, Mastercard support via TatraPay+
- Bank Transfers (BANK_TRANSFER) - With auto-generated payment details
- OAuth 2.0 Authentication - Automatic token management and caching
- ISO 20022 Status Codes - Proper handling of ACSC, RJCT, PDNG, etc.
- Webhook Support - Real-time payment status updates
- Full TypeScript - Complete type definitions
- Copy & Paste Ready - Just copy files to your project
# Clone this repository
git clone https://github.com/ZEDce/tatrapay-nextjs.git
# Copy the lib file
cp tatrapay-nextjs/src/lib/tatrapay.ts your-project/src/lib/
# Copy API routes
cp -r tatrapay-nextjs/src/app/api/payment your-project/src/app/api/
# Copy result pages (optional)
cp -r tatrapay-nextjs/src/app/payment your-project/src/app/Create .env.local in your project:
TATRAPAY_CLIENT_ID=your_client_id
TATRAPAY_CLIENT_SECRET=your_client_secret
TATRAPAY_SANDBOX=true
NEXT_PUBLIC_BASE_URL=https://your-domain.comGet credentials from Tatra banka Developer Portal.
Add these columns to track payment status:
ALTER TABLE orders ADD COLUMN tatrapay_payment_id TEXT;
ALTER TABLE orders ADD COLUMN tatrapay_status TEXT;
ALTER TABLE orders ADD COLUMN tatrapay_transaction_id TEXT;import { createPayment, getPaymentStatus, isPaymentSuccessful } from '@/lib/tatrapay'
// Create payment
const payment = await createPayment({
paymentMethod: 'CARD_PAY',
amount: { amount: 7900, currency: 'EUR' }, // 79.00 EUR
merchantReference: 'ORDER-123',
customer: {
firstName: 'John',
lastName: 'Doe',
email: '[email protected]'
},
returnUrl: 'https://your-site.com/api/payment/callback?orderId=123',
customerIpAddress: '1.2.3.4'
})
// Redirect to TatraPay
window.location.href = payment.redirectUrl
// Later, in callback - check status
const status = await getPaymentStatus(payment.paymentId)
if (isPaymentSuccessful(status.status)) {
// Payment completed!
}Creates a new payment intent.
| Parameter | Type | Required | Description |
|---|---|---|---|
paymentMethod |
'CARD_PAY' | 'BANK_TRANSFER' |
Yes | Payment method |
amount.amount |
number |
Yes | Amount in cents (7900 = 79.00) |
amount.currency |
'EUR' | 'CZK' |
Yes | Currency |
merchantReference |
string |
Yes | Your order ID (no spaces!) |
customer |
object |
No | Customer details |
returnUrl |
string |
Yes | Callback URL after payment |
customerIpAddress |
string |
Yes | Customer's IP address |
Returns:
{
paymentId: string,
status: TatraPayStatus,
redirectUrl?: string, // For CARD_PAY
bankTransferInfo?: { // For BANK_TRANSFER
iban: string,
bic: string,
variableSymbol: string
}
}Gets current payment status.
Returns:
{
paymentId: string,
status: TatraPayStatus,
authorizationStatus?: string, // 'AUTH_DONE' for successful card payments
merchantReference: string,
amount: { amount: number, currency: string },
transactionId?: string
}Important: For card payments, check BOTH status and authorizationStatus:
const isSuccessful = isPaymentSuccessful(status.status) ||
status.authorizationStatus === 'AUTH_DONE'isPaymentSuccessful(status) // true for ACCC, ACSC, ACSP, OK, AUTH_DONE, etc.
isPaymentFailed(status) // true for RJCT, CANC
isPaymentPending(status) // true for RCVD, PDNG, etc.
mapToInternalStatus(status) // 'completed' | 'failed' | 'pending'CRITICAL: TatraPay uses ISO 20022 codes, NOT human-readable names!
| Code | Meaning | Description |
|---|---|---|
ACSC |
Settlement Completed | ✅ Most common for card payments |
ACCC |
Credit Completed | ✅ Payment fully completed |
ACSP |
Settlement In Progress | ✅ Being processed |
OK |
TatraPay Success | ✅ Card payment authorized |
AUTH_DONE |
Authorization Done | ✅ Card authorization completed (check authorizationStatus field) |
| Code | Meaning | Description |
|---|---|---|
RJCT |
Rejected | ❌ Payment declined |
CANC |
Cancelled | ❌ Payment cancelled |
| Code | Meaning | Description |
|---|---|---|
PDNG |
Pending | ⏳ Awaiting processing |
RCVD |
Received | ⏳ Payment received |
src/
├── lib/
│ └── tatrapay.ts # Core TatraPay client (copy this!)
├── app/
│ ├── api/payment/
│ │ ├── create/route.ts # POST - Create payment
│ │ ├── callback/route.ts # GET - Handle redirect
│ │ └── webhook/route.ts # POST - Status webhooks
│ └── payment/
│ ├── success/page.tsx # Success page
│ ├── failed/page.tsx # Failed page
│ └── pending/page.tsx # Pending page
Problem: Card payment returns "cardDetail was not provided"
Solution: Always include cardDetail for CARD_PAY:
apiBody.cardDetail = {
cardHolder: 'Customer Name' // Max 45 chars, alphanumeric only
}Problem: Checking wrong status code
Solution: Use ISO 20022 codes:
// ❌ Wrong
if (status === 'SETTLED') { ... }
// ✅ Correct
if (isPaymentSuccessful(status)) { ... }
// Or directly: if (status === 'ACSC') { ... }Problem: Card payment succeeds (money charged) but your app shows "pending"
Root Cause: TatraPay returns status: "OK" and authorizationStatus: "AUTH_DONE" for successful card payments, but some code only checks for ISO 20022 codes (ACSC, ACCC).
Solution: Check BOTH fields:
const paymentStatus = await getPaymentStatus(paymentId)
// Check both status and authorizationStatus
const isSuccessful = isPaymentSuccessful(paymentStatus.status) ||
paymentStatus.authorizationStatus === 'AUTH_DONE'
if (isSuccessful) {
// Payment successful - update DB and send confirmation
}Note: isPaymentSuccessful() now includes OK and AUTH_DONE in success list.
Problem: tatraPayPlusUrl is undefined
Solution:
- Ensure
cardDetailis included for CARD_PAY - Check
Redirect-URIheader is set - Verify
Preferred-Methodheader matches request
Problem: TatraPay doesn't receive your custom params (voucherId, orderId)
Root Cause: Redirect-URI header must exactly match registered URL - no query params allowed.
Solution: Store paymentId in DB, look up by TatraPay's paymentId param in callback:
// Store when creating payment
await db.vouchers.update({ tatrapay_payment_id: payment.paymentId })
// In callback - TatraPay adds paymentId param
const paymentId = searchParams.get('paymentId')
const voucher = await db.vouchers.findBy({ tatrapay_payment_id: paymentId })Problem: Cannot read properties of undefined (reading 'amount')
Root Cause: TatraPay uses amountValue in some responses, amount in others.
Solution:
const value = response.amountValue ?? response.amount ?? 0Problem: Callback shows INIT status instead of ACSC after simulating success.
Root Cause: Sandbox timing - status API doesn't update instantly.
Solution: This is normal for sandbox. Production payments return proper status immediately.
Problem: TatraPay rejects phone numbers
Solution: Sanitize to E.164 format:
phone = phone.replace(/[^\d+]/g, '')
if (!phone.startsWith('+')) phone = '+421' + phone.replace(/^0/, '')Problem: Sandbox rejects 127.0.0.1 or ::1
Solution: Use fake public IP for sandbox:
headers['IP-Address'] = isSandbox ? '85.216.67.1' : realIp- Set
TATRAPAY_SANDBOX=truein.env.local - Get sandbox credentials from developer portal
- Make a test purchase
- On TatraPay page, choose:
- "Zrealizovaná platba" → Successful payment
- "Platba zlyhala" → Failed payment
- Click "Pokračovať" to return to your callback
Any valid card format works in sandbox:
4111 1111 1111 1111(Visa)5500 0000 0000 0004(Mastercard)
Before requesting production access, ensure your integration works:
- Test complete payment flow locally with
TATRAPAY_SANDBOX=true - Verify CARD_PAY works (includes
cardDetail) - Verify BANK_TRANSFER works
- Verify callback updates your database correctly
- Test both success and failure scenarios
-
Submit online form: https://api.tatrabanka.sk/onlineForm
- Organization name
- API interface: TatraPayPlus
- Brief description of your use case
-
Tatra banka will contact you for contract signing
-
After contract approval:
- Log in to Tatra banka Developer Portal
- Go to Aplikácie → Your application → Editovať
- Click on TatraPayPlus API v1.5.1
- Click edit icon next to plans
- Select "TatraPayPlus Production Plan"
- Accept terms ("PRIJÍMAM PODMIENKY POUŽÍVANIA")
- Set
TATRAPAY_SANDBOX=falsein production environment - Update
NEXT_PUBLIC_BASE_URLto production domain - Verify callback URLs are accessible from internet
- Test with small real payment (e.g., 1€)
- Verify confirmation emails are sent
- Set up error monitoring (Sentry, etc.)
| Plan | Rate Limit | Daily Quota | Use Case |
|---|---|---|---|
| Sandbox | 5 req/sec | 5000/day | Testing only |
| Production | Higher | Higher | Real payments |
Important: The Client ID and Secret remain the same for both plans - only the API endpoint changes based on TATRAPAY_SANDBOX flag.
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
MIT License - see LICENSE for details.