What you'll build
A Node.js/Express integration with DPO Pay (formerly 3G Direct Pay), a pan-African payment gateway that supports card, mobile money, and bank payments across 20+ countries. By the end of this tutorial you'll have:
- A
POST /pay/dpoendpoint that creates a payment token via DPO's XML API and returns a checkout URL - XML request/response parsing for DPO's v6 API
- A
POST /webhooks/dpoendpoint that verifies payment status using the token - Support for GHS, KES, NGN, ZAR, UGX, TZS, XOF, and other African currencies
DPO Pay handles the payment UI — you create a token, redirect the customer, and verify the result when they return.
Get your API key
- Register at dpogroup.com and complete the merchant onboarding process
- Once approved, you'll receive a Company Token — a long alphanumeric string that authenticates all API calls
- DPO's API is at
https://secure.3gdirectpay.com/API/v6/for both test and live transactions — the Company Token determines which environment you're in
Add it to your .env:
DPO_COMPANY_TOKEN=your_company_token_here
DPO_BASE_URL=https://secure.3gdirectpay.com
DPO uses a single API endpoint for both sandbox and production. Your Company Token determines the mode. DPO provides a test Company Token during onboarding — use it until you're ready to go live.
Build the payment endpoint
DPO's API is XML-based, not JSON. You'll send XML payloads and parse XML responses. The flow is: create a token, then redirect the customer to DPO's hosted payment page using that token.
Create a DPO Pay integration for a Node.js/Express app. Install the fast-xml-parser package for XML handling.
1. src/services/dpo.service.ts — export two functions:
createPaymentToken(params: { amount: number, currency: string, reference: string, redirectUrl: string, backUrl: string, customerEmail: string, customerName: string, serviceDescription: string }): Promise<{ token: string, transRef: string }> — Build an XML request body:
xml <?xml version="1.0" encoding="utf-8"?> <API3G> <CompanyToken>${DPO_COMPANY_TOKEN}</CompanyToken> <Request>createToken</Request> <Transaction> <PaymentAmount>${params.amount.toFixed(2)}</PaymentAmount> <PaymentCurrency>${params.currency}</PaymentCurrency> <CompanyRef>${params.reference}</CompanyRef> <RedirectURL>${params.redirectUrl}</RedirectURL> <BackURL>${params.backUrl}</BackURL> <CompanyRefUnique>1</CompanyRefUnique> <PTL>30</PTL> </Transaction> <Services> <Service> <ServiceType>5525</ServiceType> <ServiceDescription>${params.serviceDescription}</ServiceDescription> <ServiceDate>${new Date().toISOString().split("T")[0] + " " + new Date().toTimeString().split(" ")[0]}</ServiceDate> </Service> </Services> <Additional> <CustomerEmail>${params.customerEmail}</CustomerEmail> <CustomerName>${params.customerName}</CustomerName> </Additional> </API3G>
POST this XML with Content-Type "application/xml" to ${DPO_BASE_URL}/API/v6/. Parse the XML response using fast-xml-parser. Check that Result === "000" (success). Return { token: TransToken, transRef: TransRef } from the response. If Result is not "000", throw an error with the ResultExplanation.
getCheckoutUrl(token: string): string — Return ${DPO_BASE_URL}/payv2.php?ID=${token}. This is the hosted payment page URL.
2. src/controllers/dpo.controller.ts — export createDpoPayment(req, res, next):
- Extract amount, currency (default "GHS"), customerEmail, customerName, description from req.body
- Validate currency is one of: GHS, KES, NGN, ZAR, UGX, TZS, XOF
- Generate a unique reference: "DPO-" + Date.now() + "-" + crypto.randomBytes(4).toString("hex").toUpperCase()
- Set redirectUrl to your frontend's payment success page, backUrl to your frontend's payment cancelled page
- Call createPaymentToken with all parameters
- Save the order to your database with status "pending", the reference, and the DPO transaction token
- Return 201 with { checkoutUrl: getCheckoutUrl(token), reference, token }
3. src/routes/dpo.router.ts — POST / → createDpoPayment. Mount under /pay/dpo in your main app.
Handle the webhook / verify payment
After the customer completes (or cancels) payment, DPO redirects them to your RedirectURL with the transaction token. You must verify the payment status server-side using the verifyToken API call.
Create the DPO Pay verification handler.
1. src/services/dpo.service.ts — add a function:
verifyPaymentToken(token: string): Promise<{ result: string, resultExplanation: string, customerEmail: string, transactionAmount: string, transactionCurrency: string, transactionRef: string }> — Build an XML request body:
xml <?xml version="1.0" encoding="utf-8"?> <API3G> <CompanyToken>${DPO_COMPANY_TOKEN}</CompanyToken> <Request>verifyToken</Request> <TransactionToken>${token}</TransactionToken> </API3G>
POST to ${DPO_BASE_URL}/API/v6/ with Content-Type "application/xml". Parse the XML response. Return { result: Result, resultExplanation: ResultExplanation, customerEmail: CustomerEmail, transactionAmount: TransactionAmount, transactionCurrency: TransactionCurrency, transactionRef: CompanyRef }.
2. src/controllers/dpo.controller.ts — add verifyDpoPayment(req, res, next):
- Extract the token from req.query.TransactionToken or req.body.token
- Call verifyPaymentToken with the token
- If result === "000": payment was successful. Find the order by its DPO token, update status to "paid", save paid_at timestamp and the transaction details. Return 200 with { status: "paid", reference }
- If result === "901": token is invalid or expired. Return 400 with the resultExplanation
- If result === "904": token not yet paid. Return 200 with { status: "pending" }
- For any other result: log the result code and explanation, update order to "failed", return 200 with { status: "failed", reason: resultExplanation }
3. src/routes/dpo.router.ts — add:
- GET /verify → verifyDpoPayment (called when customer is redirected back from DPO)
- POST /verify → verifyDpoPayment (can also be called server-to-server)
Test it
Start your server and create a test payment:
curl -X POST http://localhost:3000/pay/dpo \
-H "Content-Type: application/json" \
-d '{
"amount": 100.00,
"currency": "GHS",
"customerEmail": "[email protected]",
"customerName": "Ama Serwaa",
"description": "Premium subscription"
}'
The response returns a checkoutUrl. Open it in a browser — DPO's hosted payment page displays available payment methods for the selected currency (card, mobile money, bank transfer, etc.).
With a test Company Token, DPO provides test card numbers:
- Card: 5436 8860 0001 6909
- Expiry: any future date
- CVV: 123
After payment, DPO redirects to your RedirectURL with the transaction token appended. Your verify endpoint picks up the token and confirms the payment.
To manually verify a payment:
curl http://localhost:3000/pay/dpo/verify?TransactionToken=YOUR_TOKEN_HERE
Always verify server-side. The redirect to your success page does not mean payment succeeded — the customer could have navigated there manually. The verifyToken XML call with result "000" is the only reliable confirmation. Call it on every redirect and consider polling it if you haven't received a result within a few minutes.
Project structure after this tutorial
src/
controllers/
dpo.controller.ts
routes/
dpo.router.ts
services/
dpo.service.ts
Environment variables:
| Variable | Description |
|---|---|
DPO_COMPANY_TOKEN |
Your Company Token from DPO (determines sandbox vs production) |
DPO_BASE_URL |
https://secure.3gdirectpay.com (same for both environments) |
Supported currencies:
| Currency | Countries |
|---|---|
| GHS | Ghana |
| KES | Kenya |
| NGN | Nigeria |
| ZAR | South Africa |
| UGX | Uganda |
| TZS | Tanzania |
| XOF | West Africa (BCEAO) |
BlocWeave