Subscriptions with custom checkout
Collect payment details directly on your site using MONEI Components to activate subscriptions. This gives you full control over the checkout experience.
Before You Begin
- You need a MONEI account, your API key, and your Account ID (found in Dashboard settings).
- Use your test mode keys during development.
- Ensure at least one payment method is enabled in your account settings.
Choose Your Flow
There are two ways to activate a subscription with MONEI Components:
| Token first | Activate first | |
|---|---|---|
| How it works | Collect payment details → generate token → activate with token | Activate → get payment ID → collect details → confirm payment |
| When to use | Simpler flow, fewer server roundtrips | When you need the payment object before collecting details |
| Component init | accountId + sessionId | paymentId |
Token first (recommended)
Generate a payment token on the client, then activate the subscription server-side with that token.
1. Create a subscription (Server-side)
Create a Subscription on your server. This step is identical to the prebuilt payment page flow.
- cURL
- Node.js
- PHP
- Python
curl --request POST 'https://api.monei.com/v1/subscriptions' \
--header 'Authorization: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"amount": 1500,
"currency": "EUR",
"interval": "month",
"intervalCount": 1,
"description": "Pro Plan Monthly",
"customer": {
"name": "John Doe",
"email": "john.doe@example.com"
},
"callbackUrl": "https://example.com/subscriptions/callback",
"paymentCallbackUrl": "https://example.com/payments/callback"
}'
import {Monei} from '@monei-js/node-sdk';
const monei = new Monei('YOUR_API_KEY');
const subscription = await monei.subscriptions.create({
amount: 1500,
currency: 'EUR',
interval: 'month',
intervalCount: 1,
description: 'Pro Plan Monthly',
customer: {
name: 'John Doe',
email: 'john.doe@example.com'
},
callbackUrl: 'https://example.com/subscriptions/callback',
paymentCallbackUrl: 'https://example.com/payments/callback'
});
const subscriptionId = subscription.id;
<?php
require_once 'vendor/autoload.php';
use Monei\MoneiClient;
use Monei\Model\CreateSubscriptionRequest;
use Monei\Model\PaymentCustomer;
$monei = new MoneiClient('YOUR_API_KEY');
$subscription = $monei->subscriptions->create(
new CreateSubscriptionRequest([
'amount' => 1500,
'currency' => 'EUR',
'interval' => 'month',
'interval_count' => 1,
'description' => 'Pro Plan Monthly',
'customer' => new PaymentCustomer([
'name' => 'John Doe',
'email' => 'john.doe@example.com'
]),
'callback_url' => 'https://example.com/subscriptions/callback',
'payment_callback_url' => 'https://example.com/payments/callback'
])
);
$subscriptionId = $subscription->getId();
?>
import Monei
from Monei import CreateSubscriptionRequest, PaymentCustomer
monei = Monei.MoneiClient(api_key="YOUR_API_KEY")
subscription = monei.subscriptions.create(
CreateSubscriptionRequest(
amount=1500,
currency="EUR",
interval="month",
interval_count=1,
description="Pro Plan Monthly",
customer=PaymentCustomer(
name="John Doe",
email="john.doe@example.com"
),
callback_url="https://example.com/subscriptions/callback",
payment_callback_url="https://example.com/payments/callback"
)
)
subscription_id = subscription.id
2. Add Component to your page (Client-side)
Include monei.js on your checkout page and mount the CardInput component using your accountId and a unique sessionId.
<head>
<title>Checkout</title>
<script src="https://js.monei.com/v3/monei.js"></script>
</head>
<body>
<div id="card-element">
<!-- MONEI Card Input Component will be inserted here -->
</div>
<button id="subscribe-button">Subscribe</button>
<script src="client.js"></script>
</body>
// Initialize CardInput with your accountId and a unique sessionId
const cardElement = monei.CardInput({
accountId: 'YOUR_ACCOUNT_ID',
sessionId: 'unique_session_id' // Unique per customer session
});
// Render the Component into the container
cardElement.render('#card-element');
Use a different sessionId for each customer session. This ensures the token-generating customer matches the paying customer. You must pass the same sessionId when activating the subscription in step 4.
3. Get the payment token (Client-side)
When the customer clicks the subscribe button, submit the card input to generate a one-time paymentToken.
document.getElementById('subscribe-button').addEventListener('click', async () => {
// Generate a payment token from the card input
const {token, error} = await cardElement.submit();
if (error) {
// Show the error to the customer
console.error('Error:', error);
return;
}
// Send the token and sessionId to your server
const response = await fetch('/activate-subscription', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
subscriptionId: '{{subscription_id}}',
paymentToken: token,
sessionId: 'unique_session_id'
})
});
const result = await response.json();
// Handle the result (e.g., show success message)
});
4. Activate with token (Server-side)
Activate the subscription with the paymentToken and sessionId received from the client.
- cURL
- Node.js
- PHP
- Python
curl --request POST 'https://api.monei.com/v1/subscriptions/YOUR_SUBSCRIPTION_ID/activate' \
--header 'Authorization: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"paymentToken": "token_from_client",
"sessionId": "unique_session_id"
}'
// In your /activate-subscription route handler
app.post('/activate-subscription', async (req, res) => {
const {subscriptionId, paymentToken, sessionId} = req.body;
const activation = await monei.subscriptions.activate(subscriptionId, {
paymentToken,
sessionId
});
res.json({status: activation.status});
});
<?php
use Monei\Model\ActivateSubscriptionRequest;
$body = json_decode(file_get_contents('php://input'), true);
$activation = $monei->subscriptions->activate(
$body['subscriptionId'],
new ActivateSubscriptionRequest([
'payment_token' => $body['paymentToken'],
'session_id' => $body['sessionId']
])
);
echo json_encode(['status' => $activation->getStatus()]);
?>
from Monei import ActivateSubscriptionRequest
# In your /activate-subscription route handler
activation = monei.subscriptions.activate(
body["subscriptionId"],
ActivateSubscriptionRequest(
payment_token=body["paymentToken"],
session_id=body["sessionId"]
)
)
5. Handle webhooks (Server-side)
Webhook handling is the same as the prebuilt payment page flow. MONEI sends payment callbacks to paymentCallbackUrl and subscription status changes to callbackUrl. Always verify the signature and return a 200 status code.
Activate first (alternative)
Activate the subscription first to get a payment ID, then collect payment details and confirm client-side.
1. Create a subscription
Same as step 1 above.
2. Activate the subscription (Server-side)
Activate without a paymentToken — the response is a Payment object with a payment.id.
- cURL
- Node.js
- PHP
- Python
curl --request POST 'https://api.monei.com/v1/subscriptions/YOUR_SUBSCRIPTION_ID/activate' \
--header 'Authorization: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"completeUrl": "https://example.com/checkout/complete"
}'
const activation = await monei.subscriptions.activate('YOUR_SUBSCRIPTION_ID', {
completeUrl: 'https://example.com/checkout/complete'
});
// Pass the payment ID to your client-side
const paymentId = activation.id;
<?php
use Monei\Model\ActivateSubscriptionRequest;
$activation = $monei->subscriptions->activate(
'YOUR_SUBSCRIPTION_ID',
new ActivateSubscriptionRequest([
'complete_url' => 'https://example.com/checkout/complete'
])
);
// Pass the payment ID to your client-side
$paymentId = $activation->getId();
?>
from Monei import ActivateSubscriptionRequest
activation = monei.subscriptions.activate(
"YOUR_SUBSCRIPTION_ID",
ActivateSubscriptionRequest(
complete_url="https://example.com/checkout/complete"
)
)
# Pass the payment ID to your client-side
payment_id = activation.id
Use the payment.id from the response to initialize the CardInput component — do not redirect the customer to nextAction.redirectUrl. That URL is for the prebuilt payment page flow.
3. Mount Component and confirm payment (Client-side)
Initialize the CardInput with the paymentId from the activation response, then confirm the payment.
<head>
<title>Checkout</title>
<script src="https://js.monei.com/v3/monei.js"></script>
</head>
<body>
<div id="card-element"></div>
<button id="subscribe-button">Subscribe</button>
<script src="client.js"></script>
</body>
// Initialize CardInput with the paymentId from the activation response
const paymentId = '{{payment_id}}'; // Passed from your server
const cardElement = monei.CardInput({paymentId: paymentId});
cardElement.render('#card-element');
document.getElementById('subscribe-button').addEventListener('click', async () => {
const {token, error} = await cardElement.submit();
if (error) {
console.error('Error:', error);
return;
}
// Confirm the payment with the generated token
const result = await monei.confirmPayment({
paymentId: paymentId,
paymentToken: token
});
// Show the result to the customer
// Always rely on webhooks for the definitive payment status
console.log('Payment status:', result.status);
});
4. Handle webhooks
Same as the token first flow — MONEI sends webhooks for both payments and subscription status changes.
Before You Go Live
- Switch to your live mode API key and Account ID.
- Ensure your payment methods are enabled in live mode.
- Test your webhook endpoints handle both payment and subscription callbacks correctly.
In test mode, subscriptions have the following limits:
- Maximum 3 active subscriptions per account
- Subscriptions are auto-canceled after 12 payments
- Minute and hour billing intervals are available for faster testing