Ugrás a tartalomra
kassza

IPN fizetési értesítés

Az IPN (Instant Payment Notification) a Számlázz.hu webhookja. Ha egy bizonylat (számla vagy díjbekérő) kifizetett összege megváltozik, például részleges vagy teljes befizetés után, a Számlázz.hu HTTP POST kérést küld a megadott címre. A kérés törzse application/x-www-form-urlencoded formátumú, és a számla számát, a végösszeget és a kifizetett összeget tartalmazza.

Értesítés csak akkor készül, ha a változás a számla kiállítójának oldalán történik, és a fiókban be van állítva az IPN cím. A kassza/ipn modul beolvassa és típusos objektummá alakítja az értesítést, és segít ellenőrizni, hogy a kérés tényleg a Számlázz.hu-tól jött-e.

Beállítás#

  1. A Számlázz.hu fiókban nyisd meg a Fiók beállítások → Számlázás alapadatok oldalt, és az oldal alján lévő mezőbe írd be a végpontod címét, például https://webshop.example.hu/api/szamlazz/ipn.
  2. A végpont a sikeres feldolgozás után HTTP 200 státusszal válaszoljon. A válasz törzse nem számít, csak a státuszkód.
  3. Ha a végpontot IP-cím alapján korlátozod, engedélyezd a küldő IP-címeket.

Next.js route#

app/api/szamlazz/ipn/route.ts
import { ipnOkResponse, isSzamlazzIp, readIpnNotification } from 'kassza/ipn'
import { db } from '@/lib/db'
import { rendelesVisszaigazolas } from '@/lib/rendeles'

export async function POST(request: Request) {
  if (!isSzamlazzIp(request.headers.get('x-forwarded-for') ?? '')) {
    return new Response('Tiltott', { status: 403 })
  }

  const ipn = await readIpnNotification(request)

  if (ipn.isFullyPaid) {
    const { count } = await db.rendeles.updateMany({
      where: { szamlaszam: ipn.invoiceNumber, fizetve: false },
      data: { fizetve: true, fizetettOsszeg: ipn.paidAmount },
    })
    if (count > 0) await rendelesVisszaigazolas(ipn.invoiceNumber)
  }

  return ipnOkResponse()
}

A route csak akkor jelöli fizetettnek a rendelést, és csak akkor küld visszaigazolást, ha a rendelés még nem volt fizetett. Így egy ismételten beérkező értesítés nem küld második e-mailt. Ha a feldolgozás hibát dob, a Next.js 500-as státusszal válaszol, és a Számlázz.hu később újraküldi az értesítést.

Az isSzamlazzIp() az x-forwarded-for fejléc jobb szélső címét vizsgálja, azt, amelyet a hozzád legközelebbi proxy látott. Vercelen ez a kliens címe. Több proxy mögött add meg a trustedProxies opciót, a részleteket lent olvashatod.

Más keretrendszerben#

A readIpnNotification() szabványos Request objektumot vár, így Hono, Cloudflare Workers, Bun és Deno alatt is közvetlenül használható:

import { Hono } from 'hono'
import { ipnOkResponse, isSzamlazzIp, readIpnNotification } from 'kassza/ipn'

const app = new Hono()

app.post('/szamlazz/ipn', async (c) => {
  if (!isSzamlazzIp(c.req.header('cf-connecting-ip') ?? '')) return c.text('Tiltott', 403)

  const ipn = await readIpnNotification(c.req.raw)
  if (ipn.isFullyPaid) await rendelesFizetve(ipn)

  return ipnOkResponse()
})

A kassza/ipn exportjai#

ExportLeírás
readIpnNotification(request: Request)Beolvassa a kérést, és Promise<IpnNotification>-t ad. multipart/form-data törzset is kezel, üres törzsnél az URL query paramétereit olvassa. A MAX_IPN_BODY_BYTES (64 KiB) fölötti törzset validation hibával elutasítja.
parseIpnNotification(input)Ugyanez már beolvasott adatból: szövegből, URLSearchParams-ból, FormData-ból vagy Record<string, string> objektumból.
ipnOkResponse()HTTP 200 Response, OK törzzsel.
isSzamlazzIp(ip: string | null | undefined, options?)true, ha a cím a Számlázz.hu egyik küldő címe. Az options.trustedProxies a jobbról átugrandó megbízható proxyk száma (alapérték: 0), az options.allowedIps a küldő címek egyedi listája.
SZAMLAZZ_OUTBOUND_IPSA küldő IP-címek listája.
IPN_FIELDSAz IpnNotification mezőneveit a Számlázz.hu paraméterneveire képezi.
parseIpnAmount(value: string)Egy IPN összeget alakít számmá, értelmezhetetlen szövegnél undefined.

IpnNotification#

MezőTípusIPN paraméterLeírás
invoiceNumberstringszlahu_szamlaszamKötelező. A számla száma.
proformaNumberstring | undefinedszlahu_dijbekero_szamaA díjbekérő száma, ha a számla díjbekérőből készült.
orderNumberstring | undefinedszlahu_rendelesszamA rendelésszám, ha szerepelt a számlán.
grossTotalnumberszlahu_bruttovegosszegKötelező. A számla bruttó végösszege.
paidAmountnumberszlahu_kifizetettbruttoKötelező. A kifizetett bruttó összeg.
paymentMethodstring | undefinedszlahu_fizetesmodA fizetés módja, például 'átutalás' vagy 'bankkártya'.
paymentDatestring | undefinedszlahu_kifizdatA kifizetés dátuma 'YYYY-MM-DD' alakban, ha be van kapcsolva.
isFullyPaidbooleanszámolttrue, ha a kifizetett összeg eléri a végösszeget.
rawReadonly<Record<string, string>>minden paraméterAz összes kapott paraméter szövegként, a fel nem dolgozottak is.

Az üres opcionális mező undefined lesz. Ha egy kötelező mező hiányzik, vagy egy összeg nem értelmezhető, a kassza validation kategóriájú SzamlazzError-t dob. Az összegeknél a szóközös ezres tagolást ('19 470'), a tizedesvesszőt ('12 700,50') és a tizedespontot ('12700.5') is kezeli.

Az isFullyPaid a két összeg abszolút értékét hasonlítja össze, 0,005-ös tűréssel:

const isFullyPaid = Math.abs(paidAmount) + 0.005 >= Math.abs(grossTotal)

Így a túlfizetés is teljes kifizetésnek számít, és negatív végösszegnél, például helyesbítő számlánál is helyes az eredmény. Részfizetésnél false, a hátralék ilyenkor grossTotal - paidAmount.

Kipróbálás#

A példa egy értesítést állít össze Request objektumként, ellenőrzi a küldő címét, beolvassa az értesítést, és kiírja a kapott IpnNotification objektumot és a 200-as választ.

IPN fizetési értesítés

Webhook kérés feldolgozása és a Számlázz.hu IP-címének ellenőrzése.

Futtatás a sandboxban
ipn.ts
import { , ,  } from 'kassza/ipn'

const  = new ('https://webshop.example.hu/api/szamlazz/ipn', {
  : 'POST',
  : {
    'content-type': 'application/x-www-form-urlencoded',
    'x-forwarded-for': '3.73.214.98',
  },
  : new ({
    : 'WEB-2026-128',
    : 'WEB-58213',
    : '19 470',
    : '19 470',
    : 'átutalás',
    : '2026-09-17',
  }),
})

.(
  'A Számlázz.hu IP-címéről érkezett:',
  (..('x-forwarded-for') ?? ''),
)

const  = await ()
.()

if (.) .(`A(z) ${.} rendelés teljesen kifizetve.`)

const  = ()
.(., await .())

Újraküldés és sorrend#

A Számlázz.hu nem azonnal küldi ki az értesítéseket, hanem sorba teszi őket:

  • Ha nem 200-as válasz jön, vagy a kérés időtúllépéssel ér véget, 3 percenként újrapróbálja, legfeljebb 10-szer. Utána az értesítést eldobja.
  • Nagy forgalomnál az értesítések felgyűlhetnek a küldési sorban, és csak később érkeznek meg.
  • Számlánként egyszerre csak egy értesítés vár kiküldésre. Ha közben újabb változás történik, csak a legfrissebb megy ki, a korábbiak törlődnek.

Ebből három szabály következik:

  1. Ne add össze az értesítéseket. Egy részfizetésről lehet, hogy soha nem kapsz külön értesítést. Mindig a legutóbbi paidAmount és grossTotal alapján dönts, vagy az isFullyPaid mezőt használd.
  2. Válaszolj gyorsan. A lassú munkát (e-mail, raktári rendelés) tedd háttérfeladatba, és azonnal küldd a 200-as választ. Egy lassú válasz után ugyanaz az értesítés újra megérkezhet.
  3. Legyen tartalék. Ha a végpontod 10 próbálkozásnál tovább nem volt elérhető, az értesítés elveszett. Ilyenkor a nyitott rendelések számláit kérdezd le a Számla adatainak lekérése művelettel.

Idempotens feldolgozás#

Ugyanaz az értesítés többször is megérkezhet, ezért a feldolgozásnak ismételve is ugyanazt az eredményt kell adnia. A gyakorlatban:

  • Az állapotot feltételesen frissítsd (a rendelés csak akkor legyen fizetett, ha még nem az), és a mellékhatásokat (visszaigazoló e-mail, szállítás indítása) csak akkor indítsd, ha a frissítés tényleg változtatott valamit, ahogy a fenti route teszi.
  • A rendelést a invoiceNumber vagy az orderNumber alapján keresd, ne az értesítés beérkezési sorrendje alapján.
  • Ha a számlára a saját kódod is rögzít befizetést a registerPayment() metódussal, az is a kiállító oldali változás, ezért értesítést válthat ki. Az IPN kezelőből ne rögzíts újra befizetést ugyanarra a számlára.

Küldő IP-címek#

A Számlázz.hu az értesítéseket 2025. augusztus 1. óta az alábbi címekről küldi. A korábbi címeket (18.153.1.171, 3.73.114.72, 52.59.28.5) már nem használja, ha még szerepelnek a tűzfalszabályaid között, cseréld le őket.

Kimenő IP-címMire használd
3.73.214.98IPN értesítés és egyéb, a Számlázz.hu felől érkező hívás engedélyezése
3.76.149.232IPN értesítés és egyéb, a Számlázz.hu felől érkező hívás engedélyezése
18.153.156.51IPN értesítés és egyéb, a Számlázz.hu felől érkező hívás engedélyezése

Az isSzamlazzIp() a vizsgálat előtt egységes alakra hozza a címet: vesszővel elválasztott listából a jobb szélső elemet veszi (a trustedProxies számú megbízható proxy címét átugorva), levágja a szóközöket és a portot, és kezeli a [...]:port alakot és az IPv4-et IPv6-ba ágyazó ::ffff: előtagot.

Az x-forwarded-for fejléc#

KörnyezetHonnan olvasd a kliens címét?
Vercelx-forwarded-for vagy x-real-ip: a Vercel felülírja őket, az alapbeállítás megfelel.
Cloudflare (Workers vagy proxy)cf-connecting-ip. Az x-forwarded-for jobb szélső eleme is a kliens címe, ha Cloudflare után nincs másik proxy.
Saját nginxA $proxy_add_x_forwarded_for a kliens címét fűzi a fejléc végére, ezt olvassa az alapbeállítás. Ha nginx előtt is van proxy (például load balancer), add meg { trustedProxies: 1 }-et.
Expressapp.set('trust proxy', ...) beállítás után a req.ip, a proxyk számának megfelelő értékkel.
isSzamlazzIp(request.headers.get('x-forwarded-for'), { trustedProxies: 1 })

A Számlázz.hu dokumentációja az értesítéshez nem ír le aláírást, így a küldő címén kívül nincs mivel ellenőrizned a hitelességét. Ha egy értesítés alapján nagy értékű műveletet indítasz (például kiszállítást), előtte kérdezd le a számlát a kassza.invoices.get() metódussal, és a payments lista alapján dönts.

A kimenő és bejövő forgalom többi szabályát a Hálózat és biztonság oldal foglalja össze.

Oldal szerkesztéseUtoljára frissítve: