This tutorial shows you how to integrate Payconiq by Bancontact 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 via Adyen in Chargebee Billing.
Set up Chargebee.js
Include the Chargebee.js script on your page and initialize a Chargebee instance before you start. For the script tag, the Chargebee.init options, and the tearDown() caveat, see Set up Chargebee.js.
Create a 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
Always create payment intents on your server to protect sensitive information.
Create a payment intent using the Create Payment Intent API:
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"
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;
});
}
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")
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.
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());
}
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 to check the status:
async function waitForAuthorization(intentId, MAX_RETRIES = 450) { // ~15 min at 2s interval, matches timerDurationSeconds
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`);
}
Payment status reference
For detailed information about payment intent statuses, see the Payment Intent API documentation.
| 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, 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 event via webhooks and create the subscription when the payment_intent.status is authorized.
Create a subscription (server)
Pass the ID of the successfully authorized payment_intent to Chargebee’s create a subscription API.
curl https://{site}.chargebee.com/api/v2/customers/__test__8asz8Ru9WhHOJO/subscription_for_items \
-X POST \
-u {site_api_key}: \
-d payment_intent[id]="<Id of authorized payment_intent recieved in last step.>" \
-d subscription_items[item_price_id][0]="basic-EUR" \
-d subscription_items[billing_cycles][0]=2 \
-d subscription_items[quantity][0]=1 \
-d subscription_items[item_price_id][1]="day-pass-EUR" \
-d subscription_items[unit_price][1]=100
We're always happy to help you with any questions you might have! Click here to reach out to us.