Skip to main content

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:

EndpointPurpose
POST /payment-methodsReturn available methods for the given cart
POST /createStart a transaction and return a redirect URL
POST /pushReceive webhook callbacks from the PSP
GET /redirectHandle 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:

StateWhen
PaymentInitialPayment created (set automatically by API extension)
PaymentPendingTransaction started, awaiting PSP result
PaymentSuccessPSP confirms payment succeeded
PaymentCancelledCustomer cancelled at PSP
PaymentFailurePSP 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