Adding a payment provider
Each payment provider in Evolve is a standalone service that communicates with its PSP and updates commercetools Payment objects. The checkout service orchestrates the flow and reacts to payment state changes through events. This guide walks through creating a new provider service.
1. Scaffold the service
Payment services are standalone REST services built on Fastify. Unlike domain
services (which use DomainService and GraphQL), payment providers only expose
HTTP endpoints. Create a new directory:
backend/services/payment-commercetools-<provider>/
├── src/
│ ├── index.ts # Entry point
│ ├── server.ts # ProcessManager + HTTP startup
│ ├── init.ts # Client and config initialization
│ ├── config.ts # Environment variables (extends CommercetoolsConfig)
│ ├── commercetools.ts # ClientFactory setup
│ ├── <provider>.ts # PSP client setup
│ └── routes/
│ ├── app.ts # Fastify app creation and route registration
│ ├── create-transaction.ts
│ ├── payment-methods.ts
│ ├── push.ts # Webhook handler
│ ├── redirect.ts
│ └── healthcheck.ts
├── run.ts # Local dev entry with observability
└── terraform/
Server and entry point
The entry point uses ProcessManager for lifecycle management and
startHttpServer from the framework:
// src/server.ts
import { ProcessManager } from "@evolve-framework/core";
import { startHttpServer } from "@evolve-framework/core/http/server";
import { initEnvironment } from "./init.ts";
import { createApp } from "./routes/app.ts";
export const startServer = async () => {
let app;
const pm = new ProcessManager({
start: async () => {
await initEnvironment();
app = createApp();
await startHttpServer(app, {
host: config.HTTP_HOST,
port: config.HTTP_PORT,
});
},
stop: async () => {
await app?.close?.();
},
});
await pm.start();
};
Route registration
Create the Fastify app and register routes directly:
// src/routes/app.ts
import { fastifyLogger } from "@evolve-framework/core/http/server";
import Fastify from "fastify";
export const createApp = (): FastifyInstance => {
const app = Fastify({ keepAliveTimeout: 120_000 });
app.register(fastifyLogger);
app.get("/healthcheck", healthcheckHandler);
app.post("/payment-methods", paymentMethodsHandler);
app.post("/create", createTransactionHandler);
app.post("/push", webhookHandler);
app.get("/redirect", redirectHandler);
return app;
};
2. Implement the required endpoints
Every payment provider exposes the same contract:
| Endpoint | Purpose |
|---|---|
POST /payment-methods | Return available methods for the given cart |
POST /create | Start a transaction and return a redirect URL |
POST /push | Receive webhook callbacks from the PSP |
GET /redirect | Handle customer redirect back from the PSP |
Create a transaction
The /create handler receives the order and payment details from the checkout
service, calls the PSP to start a transaction, and records an initial
Transaction on the commercetools Payment:
const createPaymentHandler = async (request, reply) => {
const { orderId, amount, currency, returnUrl } = request.body;
// Call your PSP SDK to create a transaction
const pspResult = await pspClient.createTransaction({
amount,
currency,
returnUrl,
});
// Record the transaction in commercetools
await ctClient.payments.update(paymentId, {
actions: [
{
action: "addTransaction",
transaction: {
type: "Charge",
amount: { centAmount: amount, currencyCode: currency },
interactionId: pspResult.transactionId,
state: "Pending",
},
},
],
});
return reply.send({ redirectUrl: pspResult.redirectUrl });
};
3. Verify webhook signatures
The /push endpoint must verify that incoming webhooks are authentic. If your
PSP requires raw body verification (e.g., Stripe), register fastify-raw-body
in your app. Use the signature mechanism your PSP provides:
const webhookHandler = async (request, reply) => {
const signature = request.headers["x-webhook-signature"];
const isValid = verifySignature(request.rawBody, signature, config.WEBHOOK_SECRET);
if (!isValid) {
return reply.status(401).send();
}
// Update the transaction state based on the webhook payload
const newState = mapPspStatus(request.body.status);
await updateTransactionState(request.body.transactionId, newState);
return reply.status(200).send();
};
See REST endpoints and webhooks for provider-specific verification patterns.
4. Handle payment state transitions
Update the commercetools Payment state to one of the standard states:
| State | When |
|---|---|
PaymentInitial | Payment created (set automatically by API extension) |
PaymentPending | Transaction started, awaiting PSP result |
PaymentSuccess | PSP confirms payment succeeded |
PaymentCancelled | Customer cancelled at PSP |
PaymentFailure | PSP reports a failure |
When the state changes, commercetools emits a
PaymentStatusStateTransition event. The checkout service subscribes to
this event and updates the order accordingly (e.g., marking it as paid when
all payments succeed).
5. Register with the checkout service
Add your provider's endpoint to the checkout service configuration so it knows where to route payment requests:
PAYMENT_SERVICE_ENDPOINTS='{"my-provider": "http://payment-my-provider:3000"}'
The checkout service uses this map to delegate createPayment calls to the
correct provider based on paymentMethodInfo.paymentInterface.
6. Add Terraform
Define the service infrastructure and commercetools payment states in
Terraform. Follow the patterns in existing payment services such as
backend/services/payment-commercetools-stripe/terraform/.
Further reading
- Payment architecture for the full payment flow and state machine
- REST endpoints and webhooks for webhook verification patterns per provider
- Messaging and events for how payment state transitions trigger downstream processing