๐Ÿ”Œ

Payment Gateway API

v1.0 ยท PSD2 Open Banking ยท SEPA Real-time

Accept SEPA bank-to-bank payments via PSD2 Open Banking (Powens). No card networks, no merchant accounts. Compliant with EU PSD2, GDPR, and AML directives.

โšก
Instant SEPA
Real-time bank transfers via Powens PIS.
๐Ÿ”’
Bank-grade Sec
HMAC-SHA256, bcrypt secrets, IP allowlist.
๐ŸŒ
EU Compliant
PSD2, GDPR, AML. All EU/EEA banks.
โœ…
Paste your credentials in the right panel โ†’ every Try it โ†’ button uses them automatically.

Authentication

Every API request requires two HTTP headers:

Required headers
X-Api-Key:    pk_live_<token>   # public โ€” safe to log
X-Api-Secret: sk_live_<token>   # secret โ€” server-side only, never browser
๐Ÿšซ
Never expose your secret key client-side. Store it in a server-side environment variable. Do not include it in browser JS, mobile app binaries, or git.
Header
Format
Note
X-Api-Key
pk_live_...
Safe to log. Identifies your account.
X-Api-Secret
sk_live_...
Never log. Verified with bcrypt constant-time comparison.

Environments

Environment
Base URL
When to use
Production
https://api.european-pay.eu/api/v1
Live keys (pk_live_) โ€” real SEPA transfers
Dev / Staging
https://dev.eupayv1apis.european-pay.fr/api/v1
Test keys (pk_test_) โ€” no real money moves
๐Ÿ’ก
There is no separate sandbox URL. The same API handles both environments โ€” the key prefix determines the behaviour. Use a pk_test_ key against the Dev API to build and test your integration without triggering real bank transfers. Switch to a pk_live_ key on the Production API to go live.

Select the environment in the right panel to point the API tester at the correct base URL.


Errors & Codes

Error format
{ "success": false, "code": "INVALID_API_KEY", "message": "Invalid API key." }
HTTP
Code
Meaning
400
MISSING_CREDENTIALS
X-Api-Key or X-Api-Secret header absent
400
VALIDATION_ERROR
Invalid request body
401
INVALID_API_KEY
Key not found or revoked
401
INVALID_SECRET
Secret bcrypt mismatch
403
API_ACCESS_DISABLED
Admin has not enabled gateway for your account
403
IP_NOT_ALLOWED
IP not in key's allowlist
404
NOT_FOUND
Reference not found or belongs to another account
429
RATE_LIMIT_EXCEEDED
Too many requests โ€” respect retry_after

Rate Limiting

Per API key, 60-second sliding window tracked in Redis. Exceeded requests return 429 with a retry_after seconds field.

Endpoint
Limit
Customisable
Authenticated gateway endpoints
100 req/min
Yes โ€” per key in Developer Portal
/gateway/payments/:ref/status (public)
30 req/min per IP
No

Create Payment

POST/gateway/payments

Creates a payment session, initiates Powens PIS to your IBAN, and returns both a bank authorization URL and a QR code. Offer either method to your customers.

Request Body

amount
numberrequired
Amount in EUR. Min โ‚ฌ0.01, max โ‚ฌ10,000.
Example: 29.99
currency
string
Currency code. Only EUR supported.
Example: EUR
description
stringrequired
Payment description shown to customer. Max 500 chars.
Example: Order #1042
reference
string
Your order reference. Idempotency key โ€” same reference within 30 min returns the existing payment.
Example: ORDER-1042
success_url
string
Redirect URL after successful bank auth.
Example: https://yourstore.com/success
cancel_url
string
Redirect URL if customer cancels.
Example: https://yourstore.com/cancel
metadata
object
Arbitrary key-value data. Returned in webhook events. Max 20 keys.
POST /gateway/payments
curl -X POST "https://dev.eupayv1apis.european-pay.fr/api/v1/gateway/payments" \
  -H "X-Api-Key: pk_live_YOUR_KEY" \
  -H "X-Api-Secret: sk_live_YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 29.99,
    "currency": "EUR",
    "description": "Order #1042",
    "reference": "ORDER-1042",
    "success_url": "https://yourstore.com/success",
    "cancel_url": "https://yourstore.com/cancel"
  }'

Response

{
  "success": true,
  "data": {
    "reference": "PAY-20260617-0B25921FB72918ED",
    "amount": 29.99, "currency": "EUR",
    "status": "pending_sca",
    "payment_url": "https://eu-pay.biapi.pro/...",  // Option A: redirect
    "qr_data": {
      "qrData": "https://www.european-pay.fr/pay?data=...",  // Option B: QR
      "deepLinkData": "eupay://pay?data=..."
    },
    "created_at": "2026-06-17T12:20:20.979Z"
  }
}

Payment Statuses

pendingโ†’pending_scaโ†’completed|failed|cancelled

Get Payment

GET/gateway/payments/:reference

Retrieve current status of a payment. Use for server-side checks with your full credentials.


List Payments

GET/gateway/payments

Returns a paginated list of all gateway payments, newest first.

page
integer
Page number. Default: 1.
Example: 1
limit
integer
Items per page. Max 100, default 20.
Example: 20
status
string
Filter: pending, pending_sca, completed, failed, cancelled.
Example: completed
from
ISO 8601
Filter payments created on or after this timestamp.

Refund Payment

POST/gateway/payments/:reference/refund

Issue a full or partial refund on a completed payment.

amount
number
Refund amount in EUR. Omit for a full refund. Must be โ‰ค original amount.
Example: 10.00
๐Ÿ’ก
Creates a new REF-... transaction. The original payment becomes refunded. A payment.refunded webhook is dispatched.

Verify Status (Public)

GET/gateway/payments/:reference/status
โœ…
No API key required. Safe to call from browser callback pages. Rate-limited to 30 req/min per IP.

Returns only: reference, status, amount, currency. Use in your success/cancel page to show the customer a confirmation without exposing your secret.


QR Code Payments

Every POST /gateway/payments response includes qr_data.qrData โ€” an HTTPS URL encoding a signed payment payload. Render it as a QR code; the EU Pay customer app intercepts via universal link and initiates a PIS transfer.

Decoded QR payload
{
  "payload": {
    "v": 1, "type": "gateway_qr_payment",
    "gateway_reference": "PAY-20260617-XXXXXXXX",
    "merchant": { "id": "...", "name": "My Store" },
    "amount": 29.99, "currency": "EUR",
    "expiresAt": "2026-06-17T12:30:00Z"  // 30 min TTL
  },
  "sig": "<hmac_sha256_hex>"
}
โš ๏ธ
QR codes expire in 30 minutes. Regenerate at each checkout. Use error correction level H to allow a logo overlay.

QR Integration Guide

Render QR in browser/Node
import QRCode from "qrcode";

const payment = await createPayment({ amount: 29.99, ... });
const dataUrl = await QRCode.toDataURL(payment.qr_data.qrData, {
  errorCorrectionLevel: "H",  // Required for logo overlay
  width: 400, margin: 2,
});
// <img src={dataUrl} />
Poll until completed (3s interval)
async function waitForPayment(ref, ms = 600_000) {
  const end = Date.now() + ms;
  while (Date.now() < end) {
    const { data } = await fetch(
      `/api/v1/gateway/payments/${ref}/status`
    ).then(r => r.json());
    if (data.status === "completed") return data;
    if (["failed","cancelled"].includes(data.status))
      throw new Error(data.status);
    await new Promise(r => setTimeout(r, 3000));
  }
  throw new Error("timeout");
}

Webhook Setup

Configure your Webhook URL in the Developer Portal. EU Pay POSTs signed events to your server on every payment state change.

โœ“Accept HTTPS POST (HTTP rejected in production)
โœ“Return HTTP 200 within 10 seconds
โœ“Verify HMAC-SHA256 signature before processing
โœ“Be idempotent โ€” the same event may be delivered more than once

Webhook Event Types

Event
When it fires
payment.completed
Customer authorised the bank transfer โ€” fulfill the order
payment.failed
Bank rejected or customer declined SCA
payment.refunded
A refund was processed via /refund endpoint
Example webhook payload
{
  "event": "payment.completed",
  "timestamp": "2026-06-17T12:25:30Z",
  "data": {
    "reference": "PAY-20260617-0B25921FB72918ED",
    "amount": 29.99, "currency": "EUR",
    "status": "completed",
    "merchant_reference": "ORDER-1042"
  }
}

Retry Schedule

Attempt
Delay
1st (immediate)
0s
2nd
5s
3rd
30s
4th
5 min
5th (final)
30 min

Signature Verification

Every webhook includes X-EuPay-Signature: sha256=<hex>. Always verify before processing.

๐Ÿšซ
Use raw request bytes โ€” not parsed JSON โ€” when computing the HMAC.
Webhook signature verification
# Webhooks are serverโ†’server POST requests from EU Pay to your URL.
# Test with webhook.site or ngrok during development.
curl -X POST "https://yoursite.com/webhooks" \
  -H "Content-Type: application/json" \
  -H "X-EuPay-Signature: sha256=<hmac>" \
  -H "X-EuPay-Event: payment.completed" \
  -d '{"event":"payment.completed","data":{"reference":"PAY-..."}}'

Security Guide

Credential
Store where
sk_live_... (secret)
Server env var only. Never browser, app bundle, or git.
Webhook secret
Server env var. One per environment.
pk_live_... (api key)
Safe to log. Server-side requests only.
๐Ÿšซ
Never fulfill an order based only on a redirect URL parameter. Always confirm via webhook or authenticated GET from your backend.

Testing & QA

โœ“Webhook signature verified server-side (not redirect URL params)
โœ“Order fulfilled only on payment.completed webhook
โœ“Webhook handler idempotent (same event twice = same result)
โœ“Error states handled: failed, cancelled, timeout
โœ“Secret key NOT in any browser bundle
โœ“HTTPS enforced on webhook URL
โœ“QR codes regenerated per checkout โ€” never cached

๐Ÿšง

Customer Rewards API

Award EuPay Points on payment events. Query balances and configure loyalty rules programmatically.

Coming Soon

๐Ÿšง

Subscriptions API

Recurring SEPA mandates for subscription billing via Powens PIS recurring payments.

Coming Soon

E-Invoicing API

โ— LiveFrench mandate from 2026 ยท EN16931 ยท Factur-X ยท Peppol ยท SEQINO PDP

Create, submit, and track French-law-compliant e-invoices through the SEQINO PDP(Plateforme de Dรฉmatรฉrialisation Partenaire). Designed for ERPs, accounting software, and e-commerce backends to integrate directly via API key.

โš ๏ธ
French legal requirement: From 2026, all B2B invoices between French VAT-registered entities must be submitted via a PDP (SEQINO). B2C e-reporting is already required. This API handles both automatically.

Integration Flow

๐Ÿ’ก
Steps 1 & 2 are done once in the EU Pay merchant portal โ€” not via API. The API can only be used after the merchant has enrolled and their mandate is active. If enrollment is not complete, POST /einvoice/invoices returns 403 NOT_ENROLLED with instructions.
1
Enroll (merchant portal)Merchant Portal only
Merchant logs in โ†’ Settings โ†’ E-Invoicing โ†’ Enroll. Creates SEQINO client + Peppol mandate. One-time setup, not an API call.
2
Complete KYB (merchant portal)Merchant Portal only
Merchant opens the onboarding link to verify identity via Dotfile. Once approved, mandate becomes active. Portal action only.
3
Check enrollment status (API)
GET /einvoice/enrollment โ†’ returns ready: true when mandate is active. Use as a pre-flight before submitting invoices.
4
Create invoice (API)
POST /einvoice/invoices โ†’ creates EN16931 draft + submits to SEQINO PDP โ†’ returns seqino_invoice_id. Returns 403 NOT_ENROLLED if steps 1โ€“2 not done.
5
Download FacturX PDF (API)
GET /einvoice/invoices/:id/pdf โ†’ binary PDF once seqino_status = submitted
6
Mark paid + e-reporting
POST /einvoice/invoices/:id/mark-paid โ†’ records payment + triggers B2C e-reporting to SEQINO
Method
Endpoint
Description
Scope
GET
/einvoice/enrollment
Check SEQINO enrollment + mandate status
invoices:read
POST
/einvoice/invoices
Create invoice + submit to SEQINO PDP
invoices:write
GET
/einvoice/invoices
List invoices (paginated)
invoices:read
GET
/einvoice/invoices/:id
Get invoice with live SEQINO status
invoices:read
GET
/einvoice/invoices/:id/pdf
Download FacturX PDF (binary)
invoices:read
POST
/einvoice/invoices/:id/submit
Retry SEQINO submission (after KYB completes)
invoices:write
POST
/einvoice/invoices/:id/mark-paid
Mark paid + B2C e-reporting
invoices:write
POST
/einvoice/invoices/:id/cancel
Cancel + credit note to SEQINO
invoices:write
POST
/einvoice/invoices/:id/credit-note
Issue partial/full credit note
invoices:write

Check SEQINO Enrollment

GET/einvoice/enrollment

Use as a pre-flight check before submitting invoices. Returns enrollment status and whether the mandate is active (ready: true).

// Response when fully enrolled and active:
{
  "enrolled":       true,
  "mandate_id":     "mdt_5ca4c022",
  "mandate_status": "active",
  "ready":          true,       // can submit invoices now
  "siret":          "12345678901234"
}

// Response when KYB still pending:
{ "enrolled": true, "mandate_status": "pending_kyb", "ready": false }

// Not enrolled yet:
{ "enrolled": false, "ready": false }

Try it


Create Invoice

POST/einvoice/invoices

Creates a French-law-compliant EN16931 invoice and immediately submits to SEQINO PDP. Returns the invoice record with SEQINO draft ID and submission status.

customer_name
stringrequired
Buyer / company name (EN16931 BT-44).
Example: Acme Corp
customer_email
string
Buyer email for Peppol BT-49 (recommended โ€” required for B2B Peppol routing).
Example: billing@acme.com
items
arrayrequired
Line items. Each must have: product_name, quantity (number), unit_price_ttc (number), vat_rate (0|2.1|5.5|10|20).
notes
string
Optional free-text notes on the invoice.
reference
string
Your ERP reference. Used as idempotency key โ€” same reference within 24h returns the existing invoice.
Example: ERP-INV-2026-001
discount_amount
number
Overall invoice discount in EUR (applied after line totals).
Example: 50.00
POST /api/v1/einvoice/invoices
X-Api-Key: pk_live_...
X-Api-Secret: sk_live_...

// If merchant is NOT enrolled yet โ†’ 403:
{
  "success": false,
  "code": "NOT_ENROLLED",
  "message": "E-invoicing is not enabled for this account.",
  "action": "Log in to the EU Pay merchant portal โ†’ Settings โ†’ E-Invoicing โ†’ Enroll"
}

// Once enrolled โ†’ 201 Created:
{
  "invoice_number":    "INV-2026-AB12CD-000001",
  "seqino_invoice_id": "draft_abc123",
  "seqino_status":     "submitted",
  "total_ttc":         1800.00,
  "_seqino": { "submitted": true, "mandate_status": "active" }
}
๐Ÿ’ก
If the mandate is still pending KYB, seqino_status will be mandate_pending. Call POST /einvoice/invoices/:id/submit once KYB is approved to retry.

Try it


List & Get Invoices

GET/einvoice/invoices
page
integer
Page number. Default: 1.
limit
integer
Items per page. Max 100, default 20.
status
string
Filter: draft, pending, paid, refunded, cancelled, failed.
from
ISO 8601
Filter invoices created on or after this timestamp.

Try it

GET/einvoice/invoices/:id

Returns full invoice with a live SEQINO status refresh (polls SEQINO API if seqino_status = submitted).


Download FacturX PDF

GET/einvoice/invoices/:id/pdf

Downloads the EN16931 Factur-X PDF from SEQINO once seqino_status = submitted. Returns binary PDF โ€” set Accept: application/pdf in your request.

curl https://api.european-pay.eu/api/v1/einvoice/invoices/UUID/pdf \
  -H 'X-Api-Key: pk_live_...' \
  -H 'X-Api-Secret: sk_live_...' \
  -H 'Accept: application/pdf' \
  --output facture.pdf
๐Ÿ’ก
Filename in response: Content-Disposition: attachment; filename=\"facturx-INV-2026-AB12CD-000001.pdf\"

Submit / Retry SEQINO

POST/einvoice/invoices/:id/submit

Retries SEQINO PDP submission. Use when:

โ†’Initial submission returned seqino_status: mandate_pending (KYB was not yet complete)
โ†’seqino_status: submission_failed due to a transient SEQINO error

Try it


Mark Invoice Paid

POST/einvoice/invoices/:id/mark-paid

Marks invoice as paid and triggers French B2C e-reporting to SEQINO (legal requirement for all transactions).

payment_method
stringrequired
One of: cash, card, bank, terminal, other.
Example: bank
payment_date
string
ISO date YYYY-MM-DD. Defaults to today.
Example: 2026-06-17
amount
number
Actual amount received (for partial payments). Defaults to invoice total_ttc.

Try it


Cancel Invoice

POST/einvoice/invoices/:id/cancel

Cancels a pending invoice. If the invoice was already submitted to SEQINO, automatically issues a cancellation credit note (type_code 384) to the PDP.

reason
string
Optional cancellation reason.
Example: Customer request

Try it


Issue Credit Note

POST/einvoice/invoices/:id/credit-note

Issues a credit note (avoir) against a paid invoice. Supports partial refunds. Submits a type_code 381 (refund) credit note to SEQINO PDP. Full amounts use type: "cancel" (type_code 384).

amount
number
Refund amount in EUR. Omit for full credit note (equals invoice total_ttc).
Example: 50.00
reason
string
Reason text included in the credit note.
Example: Partial refund โ€” defective item
type
string
Type of credit note: "refund" (type_code 381, default) or "cancel" (type_code 384).

Try it


Product Data Model

Products are the merchant catalog โ€” food, goods, or services. Prices are always stored as TTC (VAT-inclusive), as required by French law. price_ht and vat_amount are computed getters added to every response.

Field
Type
Notes
id
UUID
Primary key
merchant_id
UUID
FK โ†’ merchants (CASCADE delete)
name
STRING(200)
Required
price_ttc
DECIMAL
VAT-inclusive (French law)
vat_rate
DECIMAL
20 | 10 | 5.5 | 2.1
vat_code
STRING
TVA20 | TVA10 | TVA55 | TVA21 (auto-mapped)
stock_quantity
INTEGER
Default 1000
image_url
TEXT
S3 CDN URL โ€” AI-backfilled if blank
is_available
BOOLEAN
false = hidden from QR menus
price_ht
computed
price_ttc / (1 + vat_rate/100) โ€” not stored
vat_amount
computed
price_ttc โˆ’ price_ht โ€” not stored
๐Ÿ’ก
If a product has no image_url, the backend silently backfills one via AI image search (Bedrock). Images appear automatically โ€” no extra API call needed.

Catalog Endpoints

All require Authorization: Bearer JWT. Staff and accountant roles share the parent merchant catalog.

Method
Endpoint
Description
GET
/product/list
List all products (category, search, available)
GET
/product/:id
Get single product
POST
/product/create
Create a product
PUT
/product/:id
Update a product
DELETE
/product/:id
Delete a product
POST
/product/upload-image-url
Presigned S3 URL for direct image upload
POST
/product/find-image
AI image search โ€” returns CDN URL
Create product
POST /api/v1/product/create
Authorization: Bearer JWT
{
  "name":           "Cafรฉ Noisette",
  "category":       "boissons",
  "price_ttc":      3.50,
  "vat_rate":       10,
  "stock_quantity": 500
}
// Response includes computed fields:
// "price_ht": 3.1818  "vat_amount": 0.3182  "vat_code": "TVA10"

Try it


Product Image Upload (S3)

Images are uploaded directly to S3 โ€” bytes never pass through the API server.

3-step S3 upload flow
// Step 1: Get presigned URL (expires in 5 min)
POST /api/v1/product/upload-image-url
{ "filename": "coffee.jpg", "contentType": "image/jpeg" }

// Step 2: PUT bytes directly to S3 (not via API)
PUT https://s3.eu-west-3.amazonaws.com/...?X-Amz-Signature=...
Content-Type: image/jpeg
<raw image bytes>

// Step 3: Save CDN URL on the product
PUT /api/v1/product/:id
{ "image_url": "https://cdn.european-pay.fr/product-images/UUID.jpg" }
โš ๏ธ
Presigned URL expires in 5 minutes. Allowed types: image/jpeg, image/png, image/webp, image/gif.

QR Signing Algorithm

Every EU Pay QR encodes a signed JSON envelope. The HMAC signature prevents forgery โ€” the customer app rejects any mismatched payload.

routes/qr.routes.js โ€” signing flow
// 1. Build the payload
const payload = {
  v: 1,
  type: "powens_qr_payment_request",  // or "gateway_qr_payment"
  requestId: crypto.randomUUID(),
  merchant:  { id, merchantId, name, euPayId },
  amount:    29.99,    // null = customer enters any amount
  currency:  "EUR",
  expiresAt: null,
};

// 2. HMAC-SHA256 sign (server-side โ€” secret never leaves backend)
const sig = crypto.createHmac("sha256", config.jwt.secret)
  .update(JSON.stringify(payload)).digest("hex");

// 3. Base64url-encode the envelope
const encoded = Buffer.from(JSON.stringify({ payload, sig })).toString("base64url");

// 4. Build QR URL
const qrData = `https://www.european-pay.fr/pay?data=${encodeURIComponent(encoded)}`;

// Error correction MUST be level H (30%) for logo overlay:
// QRCode.toDataURL(qrData, { errorCorrectionLevel: "H", width: 900 });

Table QR Ordering

Each table gets a 32-char hex token (16 random bytes). The QR encodes only this token โ€” no product data, no price. The menu is loaded fresh each scan.

Create table
POST /api/v1/table-order/tables
Authorization: Bearer JWT
{ "name": "Table 4", "zone": "Terrasse", "capacity": 4 }
โ†’ qr_token: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
โ†’ qr_url:   https://european-pay.fr/order/UUID/a1b2c3...

// To regenerate (e.g. card stolen):
POST /api/v1/table-order/tables/:id/refresh-qr
GET menu (public)
// Public โ€” no auth โ€” rate-limited: 60 req/min per IP
GET /api/v1/table-order/menu/UUID/a1b2c3...

// Security:
// โ€ข merchantId must be valid UUID format
// โ€ข token must be 32 hex chars
// โ€ข Both invalid โ†’ same 404 (prevents enumeration)
// โ€ข table.status must be 'active'

// Response:
{ "table": {...}, "merchant": {...},
  "menu": { "boissons": [...], "plats": [...] },
  "offers": [...], "productOffers": [...] }
POST submit order (public)
// Rate-limited: 10 orders / 5 min per IP
POST /api/v1/table-order/menu/UUID/a1b2c3...
{
  "items":      [{ "productId": "3f8a...", "quantity": 2, "notes": "sans sucre" }],
  "orderType":  "dine_in",   // "dine_in" | "takeaway"
  "payNow":     false         // true โ†’ Powens PIS initiated
}

// Server enforces:
// โ€ข Max 30 items per order
// โ€ข All productIds must belong to THIS merchant (prevents injection)
// โ€ข All products must have is_available = true
// โ€ข Takeaway always pay_now (server-side, not client-trusted)
// โ€ข All strings stripped of HTML tags (XSS prevention)

Merchant Payment QR

The merchant personal QR โ€” displayed in the app drawer, on printed cards, or embedded in invoices.

POST/qr/generate
amount
number
Fixed EUR amount. Omit for open-amount QR.
Example: 9.99
label
string
Payment label shown to customer.
Example: Payment to My Shop
expiresInMinutes
integer
QR TTL in minutes. Omit for permanent QR. Max 1440 (24h).
Example: 30

Try it


QR Scan โ†’ Pay Flow

The EU Pay customer app calls two sequential endpoints after scanning a QR code:

1
POST /qr/scan: Decodes + verifies HMAC. Returns merchant info and fixed amount. No money movement.
POST /api/v1/qr/scan   Authorization: Bearer customer_jwt
{ "qrData": "https://european-pay.fr/pay?data=eyJ..." }
โ†’ { merchantName, amount, fixedAmount, label, expiresAt }
2
POST /qr/pay: Customer confirms. Backend resolves merchant IBAN, initiates Powens AIS, creates Transaction, returns SCA webview URL.
POST /api/v1/qr/pay   Authorization: Bearer customer_jwt
{
  "qrData":    "https://european-pay.fr/pay?data=eyJ...",
  "accountId": "customer_bank_account_id",
  "amount":    9.99
}
โ†’ { webauthUrl, transferId, transactionId, amount }
// App opens webauthUrl in WebView โ†’ customer authorises at bank
โœ…
After SCA, Powens fires a webhook โ†’ backend marks Transaction completed โ†’ merchant push notification sent.

Gateway QR Type

Gateway payments return a qr_data object with type: "gateway_qr_payment". The gateway_reference field links the QR scan back to the original gateway transaction.

Gateway QR payload + completion chain
// Decoded gateway QR payload:
{
  "v": 1,
  "type": "gateway_qr_payment",           // distinguishes from merchant QR
  "requestId":         "PAY-20260617-...",
  "gateway_reference": "PAY-20260617-...",  // links back to gateway transaction
  "merchant": { "id": "...", "name": "My Store" },
  "amount": 29.99, "currency": "EUR",
  "expiresAt": "2026-06-17T20:50:00Z"
}

// Completion chain:
// 1. Customer scans โ†’ POST /qr/pay โ†’ stores metadata.qr.gateway_reference
// 2. Powens webhook โ†’ marks QR transaction completed
// 3. Webhook handler finds gateway_reference โ†’ marks gateway transaction completed
// 4. Fires merchant webhook_url: { event: 'payment.completed' }

End-to-End Integration Diagram

Complete flow from product catalog creation through to payment confirmation and loyalty points.

Actor
Action
Outcome
Merchant Portal
POST /product/create
Catalog built โ€” products in DB
Merchant Portal
POST /qr/generate OR POST /table-order/tables
QR created โ€” signed payload + token
Customer
Camera scans QR
Browser/app opens /pay or /order page
Customer App
POST /qr/scan
Payload decoded + HMAC verified
Customer App
POST /qr/pay
Powens AIS transfer initiated
Customer
Opens webauthUrl WebView
Bank SCA โ€” authorize transfer
Powens
POST /webhooks/powens
Transfer confirmed โ†’ Transaction completed
Backend
Push notification โ†’ merchant
'Payment received โ‚ฌ9.99'
Backend
Award EUP loyalty points โ†’ customer
If loyalty_eligible = true
Gateway (alt.)
POST /gateway/payments
Returns payment_url + qr_data
Gateway (alt.)
Powens PIS โ†’ webauthUrl
Bank transfer: customer โ†’ merchant IBAN
Gateway (alt.)
Webhook โ†’ merchant webhook_url
'payment.completed' event fired
QR Payment (in-app)
โ†’ Customer scans QR โ†’ /qr/scan โ†’ /qr/pay
โ†’ Powens AIS: customer bank โ†’ merchant IBAN
โ†’ Webhook โ†’ Transaction completed
โ†’ Push notification to merchant
Gateway API (3rd-party)
โ†’ POST /gateway/payments โ†’ payment_url + qr_data
โ†’ Powens PIS: customer bank โ†’ merchant IBAN
โ†’ Webhook โ†’ gateway transaction completed
โ†’ merchant webhook_url fired: payment.completed
API Credentials
๐Ÿงช
API Tester
Click Try it โ†’ next to any endpoint to test it here live.