After a Customer completes the first checkout, subsequent Payment actions don't need to be customer-driven through an online checkout page.
Many real-world business cases initiate Payment on the server side, such as in the following scenarios:
- Recurring Orders and recurring billing
- Automated retries after a temporary failure
- Back-office operations such as delayed capture or partial refunds
Server-driven payments let your backend securely take control of these scenarios while keeping consistency with UI-driven checkout flows.
In this section of the module, you'll learn how to implement server-driven payments by using commercetools Checkout. You'll use the following two APIs:
- Transactions API to start a Payment programmatically (without the Checkout UI)
- Payment Intents API to manage what happens after the Payment has been authorized — including capturing, cancelling, or refunding
Transactions API
To start a payment, your BFF calls the Transactions API to link the Cart with a specific Payment Integration like Stripe, Adyen, or Paypal.
async createTransaction(input: {
cartId: string;
centAmount: number;
currencyCode: string;
paymentIntegrationId: string;
applicationKey?: string;
idempotencyKey?: string;
}) {
const token = await this.getCheckoutToken(
`manage_checkout_transactions:${this.projectKey} manage_project:${this.projectKey}`
);
const body = {
application: { typeId: 'application', key: input.applicationKey ?? 'demo-key' },
cart: { typeId: 'cart', id: input.cartId },
transactionItems: [
{
paymentIntegration: { typeId: 'payment-integration', id: input.paymentIntegrationId },
amount: { centAmount: input.centAmount, currencyCode: input.currencyCode },
},
],
};
const resp = await fetch(`${this.checkoutHost}/${this.projectKey}/transactions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
...(input.idempotencyKey ? { 'Idempotency-Key': input.idempotencyKey } : {}),
},
body: JSON.stringify(body),
});
if (!resp.ok) throw new Error(`Create transaction failed: ${resp.status} ${await resp.text()}`);
return resp.json();
}
The following diagram shows how a tokenized Payment flow works by using the Checkout Transactions API. This flow lets you trigger secure, headless Payments-such as for Subscriptions or phone orders—by reusing a stored Payment token without involving the Checkout UI:
This consistency ensures the following benefits:
- You can rely on the same Messages and events for downstream processing.
- Payment statuses are updated in real time.
- Any business logic or automation that depends on these events (like Order creation or notifications) continues to work seamlessly.
In short, the Transactions API gives you full control and flexibility while preserving the trusted and robust Checkout backend behavior.
Payment Intents API
capturePayment), void an authorization (cancelAuthorization), or issue a refund (refundPayment).capturePayment action to settle the funds.async capturePayment(paymentId: string, centAmount?: number, currencyCode?: string) {
const { hasAuthSuccess, fullAmount } = await this.getPaymentState(paymentId);
if (!hasAuthSuccess) throw new Error('Payment is not authorized yet.');
const token = await this.getCheckoutToken(
`manage_checkout_payment_intents:${this.projectKey} manage_project:${this.projectKey}`
);
const amount = {
centAmount: centAmount ?? fullAmount.centAmount,
currencyCode: currencyCode ?? fullAmount.currencyCode,
};
const r = await fetch(`${this.checkoutHost}/${this.projectKey}/payment-intents/${paymentId}`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ actions: [{ action: 'capturePayment', amount }] }),
});
if (!r.ok) throw new Error(`Capture failed: ${r.status} ${await r.text()}`);
return r.json();
}
cancelAuthorization action to release the funds.async cancelAuthorization(paymentId: string) {
const token = await this.getCheckoutToken(
`manage_checkout_payment_intents:${this.projectKey} manage_project:${this.projectKey}`
);
const r = await fetch(`${this.checkoutHost}/${this.projectKey}/payment-intents/${paymentId}`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ actions: [{ action: 'cancelPayment' }] }),
});
if (!r.ok) throw new Error(`Cancel authorization failed: ${r.status} ${await r.text()}`);
return r.json();
}
refundPayment action.async refundPayment(paymentId: string, centAmount: number, currencyCode = 'EUR') {
const token = await this.getCheckoutToken(
`manage_checkout_payment_intents:${this.projectKey} manage_project:${this.projectKey}`
);
const r = await fetch(`${this.checkoutHost}/${this.projectKey}/payment-intents/${paymentId}`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ actions: [{ action: 'refundPayment', amount: { centAmount, currencyCode } }] }),
});
if (!r.ok) throw new Error(`Refund failed: ${r.status} ${await r.text()}`);
return r.json();
}
Key takeaways
- The Transactions API enables fully programmatic, headless Payments by letting you initiate tokenized Payments without the Checkout UI.
- Store Payment tokens securely after an initial Checkout Session to reuse for future transactions like Subscriptions or recurring billing.
- The Transactions API communicates with the same Payment Connectors used in standard Checkout flows, ensuring consistency in Payment lifecycle management.
- The Payment Intents API provides control over post-authorization actions including capture, cancellation, and refunds.
- Use
capturePaymentto settle authorized funds, typically after goods are shipped or services are delivered. - Use
cancelAuthorizationto release funds if an Order is cancelled before shipment or due to fulfillment issues. - Use
refundPaymentto return money to Customers after a Payment has been captured, supporting both partial and full refunds. - Server-driven Payments maintain the same Payment lifecycle stages as UI-driven checkouts, including Payment status updates and Checkout messages.