This repository contains a payment webhook processor application that integrates payment providers with fiscal systems. Currently it supports Stripe → FINA flow, but it is designed to support multiple payment providers and fiscal systems in the future.
Current Implementation: Stripe payments → Croatian FINA fiscalization. As defined by Croatian fiscalization law, it is mandatory to issue fiscal receipts when the payment is made through Payment Cards including Stripe.
Important: FINA fiscalization only supports EUR currency. Payments in other currencies will be rejected.
When a payment is made through Stripe, this service (payment-hook) performs the following steps:
- Receives a webhook from Stripe (
payment_intent.succeededevent) - Stores the webhook data in S3-compatible storage (organized by timestamp and event ID)
- Issues a fiscal receipt via FINA:
- Generates ZKI (protective code)
- Creates and signs XML
- Sends to FINA endpoint
- Stores the result:
- Saves signed request and response files to S3 storage
- Saves fiscal receipt data in database
- All files for one transaction are organized in a single S3 folder
- Fina Demo Certificates: https://www.fina.hr/finadigicert/certifikati-za-testiranje-i-demonstraciju/fina-demo-ca-certifikati
- Fina Production Certificates https://www.fina.hr/eng/finadigicert/ca-certificates
- Stripe Test Api Keys https://dashboard.stripe.com/test/apikeys
- Stripe Live Api Keys https://dashboard.stripe.com/apikeys
- Another Open Source related project https://github.com/senko/fiskal-hr
Apart standard docker python project files:
payment-hook/
├── app.py # Flask app that receives webhooks
├── fina.py # FINA fiscalization logic
├── s3_storage.py # S3-compatible storage module
├── migrate.py # Database migration system
├── .env # Secrets (Stripe, FINA, S3 settings) - should be added during deployment
├── cert/ # FINA certificate files - should be added during deployment
└── migrations/ # SQL migration files
- Docker
- FINA demo or production certificate and settings for registered account
- Stripe account
- S3-compatible storage (e.g., Hetzner Object Storage, AWS S3, MinIO)
This docker compose configuration is designed for local development and testing only. It doesn't contain SSL termination etc.
Production deployment is intended to use docker image built from this repository but with separate ingress and database services, without stripe-cli service and docker compose.
- Add
cert/to the project root with FINA certificate files. - Create a
.envfile in the project root, use.env.exampleas a template. - Configure S3-compatible storage credentials:
S3_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxx S3_SECRET_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx S3_ENDPOINT_URL=https://hel1.your-objectstorage.com S3_BUCKET_NAME=my-bucket-name
docker compose up --pull always --buildIn the docker compose up ... command output, look for the webhook signing secret from the Stripe CLI:
stripe-cli-1 | Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Add this secret to your .env file:
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Then stop the containers issuing Ctrl+C.
Then start the containers again to apply the env var changes:
docker compose up --pull always --build # --pull always --build is not needed on this specific restart, just to use history commandNote: FINA only accepts EUR currency. Use the --override flags to test with EUR:
docker compose exec stripe-cli sh -c 'stripe trigger payment_intent.succeeded --override payment_intent:currency=eur --override payment_intent:amount=1234 --api-key $STRIPE_API_SECRET_KEY'Stripe amount format: Stripe API uses the currency's minor unit (cents for EUR). The amount 1234 represents 12.34 EUR (1234 cents). The application automatically converts cents to the major unit (EUR) for FINA fiscalization.
Without the currency override, the default USD payment will be rejected with error: "FINA fiscalization only supports EUR currency".
Check the database for the stored event and the fiscal receipt. Also verify that files are stored in your S3 bucket with the structure:
YYYY-MM-DD-HH-MM-SS-stripe-payment-intent-{event_id}-{hostname}-{pid}/
├── stripe-webhook.json # Raw Stripe webhook payload
├── stripe-webhook.yaml # Parsed webhook data
├── fina-request.xml # SOAP request sent to FINA
├── fina-request.yaml # Parsed request data
├── fina-response.xml # SOAP response from FINA
└── fina-response.yaml # Parsed response data
Folder naming includes:
- UTC timestamp for chronological ordering
- Event ID for traceability to Stripe event
- Hostname to distinguish between environments (dev/production/staging)
- Process ID to handle multiple workers/containers
This prevents conflicts when the same Stripe event is sent to multiple environments (e.g., via stripe-cli to local dev and webhook to production simultaneously).
This section describes how to deploy the application to a production or test environment (non-local development).
- External PostgreSQL database (PostgreSQL 17 or compatible)
- Nginx or similar reverse proxy with SSL/TLS termination
- Docker runtime environment
- S3-compatible storage bucket
- FINA certificate files
- Stripe account (test or production)
Deploy an external PostgreSQL database. No special configuration is required - standard PostgreSQL setup is sufficient.
Create an nginx configuration file to proxy requests to the application container:
upstream payment-hook {
server localhost:8000;
}
server {
listen 443 ssl http2;
server_name payment-hook.example.com;
# SSL certificate configuration
ssl_certificate /path/to/fullchain.cer;
ssl_certificate_key /path/to/key.key;
include snippets/ssl-params.conf;
root /var/www/payment-hook/static/public;
charset UTF-8;
autoindex off;
location / {
proxy_pass http://payment-hook;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
# Preserve original request body for webhook validation
proxy_buffering off;
proxy_request_buffering off;
}
}Important nginx settings:
proxy_buffering offandproxy_request_buffering offpreserve the original webhook payload for signature verification- Timeouts should match or exceed
GUNICORN_TIMEOUTenvironment variable
Build the Docker image (typically done via CI/CD pipeline):
docker build -t payment-hook:latest .Run the container with required environment variables and volume mounts:
docker run -d \
--name payment-hook \
--restart unless-stopped \
-p 8000:8000 \
-v /path/to/cert:/app/cert:ro \
-e APP_ENV=production \
-e TIMEZONE=Europe/Zagreb \
-e S3_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxx \
-e S3_SECRET_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
-e S3_ENDPOINT_URL=https://hel1.your-objectstorage.com \
-e S3_BUCKET_NAME=my-bucket-name \
-e P12_PATH=cert/1111111.1.F1.p12 \
-e P12_PASSWORD=xxxxxxxxxxxxxxx \
-e FINA_ENDPOINT=https://cistest.apis.hr/app/ci/rc \
-e OIB_COMPANY=11111111111 \
-e OIB_OPERATOR=11111111111 \
-e LOCATION_ID=Online \
-e REGISTER_ID=1 \
-e STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxx \
-e PG_HOST=pg \
-e PG_PORT=5432 \
-e PG_USER=paymenthook \
-e PG_PASSWORD=xxxxxxxxxxxxxxx \
-e PG_DB=paymenthook \
-e GUNICORN_WORKERS=2 \
-e GUNICORN_TIMEOUT=60 \
payment-hook:latestVolume mounts:
/path/to/cert:/app/cert:ro- Mount FINA certificate directory (read-only)
Environment variables:
APP_ENV- Set toproductionordevelopment/devFINA_ENDPOINT- Use test endpoint for staging:https://cistest.apis.hr/app/ci/rcFINA_ENDPOINT- Use production endpoint for production:https://cis.porezna-uprava.hr:8449/FiskalizacijaServiceSTRIPE_WEBHOOK_SECRET- Obtain from Stripe Dashboard (see next step)- All other variables as documented in the Configuration section
- Log in to Stripe Dashboard
- Navigate to Developers → Webhooks
- Click Add endpoint
- Configure the webhook:
- Endpoint URL:
https://payment-hook.example.com/stripe/payment-intent - Events to send: Select
payment_intent.succeeded - API version: Use latest or match your Stripe SDK version
- Endpoint URL:
- After creating the webhook, copy the Signing secret (
whsec_...) - Add the signing secret to your deployment as
STRIPE_WEBHOOK_SECRETenvironment variable - Restart the container to apply the new secret
Connect your e-commerce platform (WordPress/WooCommerce, Shopify, custom app, etc.) to use your Stripe account:
- For test environment: Use Stripe test API keys
- For production environment: Use Stripe live API keys
The platform will process payments through Stripe, and Stripe will send payment_intent.succeeded webhooks to your deployed application.
After a successful payment is processed:
- Check application logs:
docker logs payment-hook - Verify database record: Query the
fina_receipttable for the transaction - Verify S3 storage: Check your S3 bucket for the transaction folder with all files
- Check FINA receipt: Verify ZKI and JIR values are populated in the database
Health check endpoint: https://payment-hook.example.com/health
Expected response when healthy:
{
"status": "healthy",
"database": "connected",
"environment": "complete",
"timestamp": "2025-10-24T12:34:56+01:00"
}WARNING: Running multiple instances that receive the same Stripe webhooks will result in duplicate fiscal receipts being issued for a single payment.
This issue occurs when:
- Local development + production: stripe-cli forwards webhooks to localhost AND Stripe sends webhooks to production deployment
- Multiple deployments: Two or more production/test deployments are both configured as webhook endpoints in Stripe
- Multiple webhook endpoints: Same Stripe account has multiple webhook endpoints pointing to different instances
Each instance that receives a payment_intent.succeeded webhook will:
- Process the payment independently
- Generate a unique fiscal receipt with a new receipt number
- Issue a separate FINA fiscalization request
- Store separate records in their respective databases
This means one payment = multiple fiscal receipts, which is incorrect for accounting and compliance purposes.
Choose ONE of these approaches:
-
During Development:
- Use stripe-cli for local testing only
- Do NOT configure production webhook endpoint during local development
- OR temporarily disable production webhook endpoint while testing locally
-
For Multiple Environments:
- Create separate Stripe accounts for different environments:
- Stripe Test account → Development/staging deployments
- Stripe Live account → Production deployment
- Each account has its own webhook configuration
- Test payments go to test account only
- Real payments go to live account only
- Create separate Stripe accounts for different environments:
-
For Production Deployments:
- Configure only ONE webhook endpoint per Stripe account
- Use load balancer if you need multiple application instances for high availability
- All instances should share the same database to prevent duplicate processing
If duplicate fiscal receipts have been issued:
- Check database for multiple records with the same
stripe_id - Check S3 storage for multiple folders with the same event ID (but different hostname/PID)
- Contact FINA support to cancel incorrect fiscal receipts if necessary
Best practice: Always ensure only one instance receives webhooks for any given Stripe payment.
- Webhook signature validation fails: Ensure nginx has
proxy_buffering offandproxy_request_buffering off - Database connection errors: Verify PostgreSQL credentials and network connectivity
- S3 upload failures: Check S3 credentials and bucket permissions
- FINA fiscalization errors: Verify certificate path, password, and endpoint URL
- Currency errors: Ensure all payments are in EUR (FINA requirement)
- Duplicate fiscal receipts: See "Multiple Instances Caveat" section above
If needed, add git pre-push hook before pushing changes to ensure code quality:
ln -s ../../.githooks/pre-push .git/hooks/pre-pushThis will automatically run code formatting and linting checks before each push.
- Fiscalization of B2C transactions with a country dependent VAT
- Operations other then fiscalize: correct, cancel, delayed fiscalization