# Integrate Payconiq by Bancontact

This tutorial shows you how to integrate Payconiq by Bancontact (opens new window) payments using Chargebee.js. Payconiq by Bancontact is a European mobile payment solution that enables customers to pay by scanning QR codes with their smartphone app.

# Supported regions

Payconiq by Bancontact is available in:

  • Belgium
  • Netherlands
  • Luxembourg

# What you'll build

By the end of this tutorial, you'll have a working integration that:

  • Creates payment intents for Payconiq by Bancontact.
  • Handles Payconiq payment flow.
  • Creates subscriptions after successful payment authorization.

# Prerequisites

Before you begin, ensure you have enabled Payconiq by Bancontact (opens new window) via Adyen in Chargebee Billing.

# Set up Chargebee.js

# Include the Chargebee.js script

To use Chargebee.js, you must include the script in your HTML page. You only need to do this once per page.

<script src="https://js.chargebee.com/v2/chargebee.js"></script> 
1

# Initialize a Chargebee instance

After including the script, initialize Chargebee.js in your JavaScript code. The initialization creates a Chargebee instance object that you can use to access Chargebee.js features.

Initialize Chargebee.js after the page has loaded and provide your site configuration. This object is used to create components.

# Example

const chargebee = Chargebee.init({
  site: "YOUR_CHARGEBEE_BILLING_SUBDOMAIN", // Your test site.
  domain: "https://mybilling.acme.com", // Optional: custom domain.
  publishableKey: "test__" 
});
1
2
3
4
5

WARNING

If you want to reinitialize Chargebee.js within the same session, call tearDown() to clean up before calling init() again.

# Create a payment intent

About `payment_intent`

A payment_intent resource manages a customer's payment session. It tracks the amount, currency, payment status, and failed payment attempts, and helps prevent duplicate charges for the same session. The payment_intent automatically updates its status based on authorization and capture events, and issues refunds if an error occurs after payment.

# Server-side implementation

Security

Always create payment intents on your server to protect sensitive information.

Create a payment intent using the Create Payment Intent API (opens new window):

curl https://{site-name}.chargebee.com/api/v2/payment_intents \
    -u {fullaccess_api_key}: \
    -d amount=500 \
    -d currency_code="EUR" \
    -d payment_method_type="payconiq_by_bancontact"
1
2
3
4
5

# Client-side implementation

Call your server endpoint from the frontend:

function createPaymentIntent() {
	return fetch('/payment-intents', {
		method: 'POST',
		headers: {
			"Content-Type": "application/json"
		},
		body: JSON.stringify({
			amount: 500,
			currency_code: 'EUR',
			payment_method_type: 'payconiq_by_bancontact'
		})
	}).then(function(response) {
		return response.json();
	}).then(function(responseJson) {
		return responseJson.payment_intent;
	});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Implement the payment flow

# Step 1: Load the payment method

Load the Payconiq by Bancontact integration using the load method:

cbInstance.load("payconiq_by_bancontact")
1

# Step 2: Handle the payment

Use the handlePayment method to initiate the payment process.

When you call handlePayment(), it opens a modal popup overlay on your page that displays a QR code for the customer to scan with their Payconiq app. The modal stays open while the customer scans the QR code and completes the payment. It closes automatically when authorization completes.

Important

For Payconiq by Bancontact, the handlePayment() promise resolves early with a payment intent that has status in_progress (not authorized). The promise resolves as soon as the modal opens and the QR code is displayed, before the customer completes the payment. You must poll for authorization before creating a subscription.

cbInstance.load("payconiq_by_bancontact").then(() => {
	cbInstance.handlePayment("payconiq_by_bancontact", {
		paymentIntent: () => {
			return createPaymentIntent();
		},
		renderInfo: {
  		    heading: "Scan QR code",
            timerLabel: "This QR code is valid for {time}", // timer will be reflected in place of {time}
            timerDurationSeconds: 15 * 60, // 15 minutes
            buttonText: "Continue to Payconiq by Bancontact",
		}
	}).then(intent => {
		// Payment intent status is `in_progress` at this point - customer needs to scan QR code.
		// Poll for authorization before creating subscription.
		return waitForAuthorization(intent.id).then(authorizedIntent => {
			return createSubscription(authorizedIntent.id);
		});
	}).catch(err => {
		console.error('Payment failed:', err);
	});
});

function createSubscription(paymentIntentId) {
	return fetch('/subscriptions', {
		method: 'POST',
		headers: {
			"Content-Type": "application/json"
		},
		body: JSON.stringify({
			paymentIntentId: paymentIntentId,
			plan_id: 'pro_plan',
			plan_quantity: 1,
			billingAddress: {
				// Add billing address details
			},
			customer: {
				// Add customer details
			}
		})
	}).then(response => response.json());
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

TIP

For complete parameter documentation, see the API Reference.

# Step 3: Monitor payment status

After calling handlePayment(), the payment intent status is in_progress while the customer scans the QR code. Poll the payment intent status until it becomes authorized or expires.

Use the Retrieve payment intent API (opens new window) to check the status:

async function waitForAuthorization(intentId, MAX_RETRIES = 10) {
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
    const response = await fetch(`/payment-intents/${intentId}`, {
      method: "GET",
      headers: {
        "Content-Type": "application/json"
      }
    });
    
    const intent = await response.json();
    const status = intent?.payment_intent?.status;
    
    if (status === "authorized") {
      return intent.payment_intent;
    }
    if (status === "expired") {
      throw new Error(`Payment ${status}`);
    }
    
    // If not done, wait before the next attempt
    await new Promise(r => setTimeout(r, 2000));
  }
  throw new Error(`Authorization not received after ${MAX_RETRIES} retries`);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# Payment status reference

For detailed information about payment intent statuses, see the Payment Intent API documentation (opens new window).

Status Description Action
inited Payment intent created Not applicable in this flow
in_progress Customer is scanning QR code Continue polling
authorized Payment successful Create subscription
consumed Payment used for subscription Do not reuse
expired Payment timed out Show error, allow retry

Use webhooks for production

Use webhooks (opens new window) for production use, instead of making the subscription creation request from the frontend, it's more secure and reliable to respond to webhooks from Chargebee on the backend. Listen to the payment_intent_updated (opens new window) event via webhooks and create the subscription when the payment_intent.status (opens new window) is authorized.

# Create a subscription (server)

Pass the ID of the successfully authorized payment_intent to Chargebee’s create a subscription API (opens new window).

  • Curl