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:
- Subscriptions 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
Here's how the API works for tokenized payments:
- After a customer completes an initial Checkout Session by using their Payment method (like a credit card), the payment service provider (PSP) can return a token that represents the Payment method.
- You can securely store this token in your system (for example, linked to the Customer profile) and reuse it for future transactions without requiring the user to re-enter their Payment details.
- By using the Transactions API, you can then trigger a new Payment at any time by sending a request that contains the token along with a reference to the Cart and the required Payment details.
- The API communicates with the same Payment Connector that's used in standard Checkout flows, and the same Payment lifecycle applies—including Payment status updates and Checkout Messages.
This API enables fully programmatic, headless Payments while still leveraging the robust and secure commercetools Checkout backend—perfect for automating recurring charges or processing deferred payments.
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
After you create a Payment resource (whether by using a Transaction API call or through a traditional checkout flow), the Payment Intents API lets you control what happens next in the Payment lifecycle.
With this API, you can perform the following key actions that normally occur after a Payment has been authorized:
capturePayment: settle the funds that were previously authorized, typically after goods are shipped.cancelAuthorization: void an authorization if the Payment shouldn't proceed, such as when a Customer cancels an Order before shipment.refundPayment: return money to a Customer, either partially or fully, after a Payment has already been captured.
These operations are performed against the Payment resource that Checkout creates during Payment authorization. This ensures that you're always working within commercetools' consistent Payment model.
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.