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.
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.
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.
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.
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.
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" }
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/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.