Payment Proxy API Documentation
Version 1.1.0 | Last Updated: November 2025
Payment Proxy is a Laravel-based payment gateway proxy service that enables multiple webstores to use a single Duitku merchant account. The service handles payment creation, status tracking, and automatic callback forwarding with retry mechanisms.
Key Features
Single Account
One Duitku account for multiple webstores
Auto Order ID
Auto-generated merchant order ID with structured format
API Key Auth
Secure API key authentication for each webstore
Auto Retry
Automatic callback forwarding with 3x retry mechanism
Authentication
All protected API endpoints require authentication using an API key.
API Key Format
EP-{16 random uppercase characters}
Example:
EP-BBZZD1PC5OCRBPTE
How to Authenticate
Include the API key in the request header:
X-API-Key: YOUR_API_KEY_HERE
Endpoint URL
https://payment.elfadigital.my.id
Create Payment
/api/payment/create
Creates a new payment request through Duitku payment gateway. This endpoint validates the request, generates a unique merchant order ID, and returns a payment URL for the customer.
Request Headers
Content-Type: application/json
X-API-Key: YOUR_API_KEY
Request Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
webstoreOrderId |
string | Yes | Unique order ID from your webstore |
paymentAmount |
integer | Yes | Payment amount in IDR (minimum: 1) |
productDetails |
string | Yes | Description of the product/service |
email |
string | Yes | Customer email address |
phoneNumber |
string | Yes | Customer phone number |
itemDetails |
array | Yes | Array of items with name, price, quantity |
customerDetail |
object | Yes | Customer information object |
returnUrl |
string | Yes | URL to redirect after payment |
expiryPeriod |
integer | No | Payment expiry in minutes (5,10,15,30,60). Default: 15 |
Example Request
curl -X POST https://payment.elfadigital.my.id/api/payment/create \
-H "Content-Type: application/json" \
-H "X-API-Key: EP-BBZZD1PC5OCRBPTE" \
-d '{
"webstoreOrderId": "WS-2025-001",
"paymentAmount": 250000,
"productDetails": "Premium Package - 1 Month Subscription",
"email": "customer@example.com",
"phoneNumber": "081234567890",
"returnUrl": "https://mywebstore.com/payment/success",
"expiryPeriod": 30,
"itemDetails": [
{
"name": "Premium Package",
"price": 250000,
"quantity": 1
}
],
"customerDetail": {
"firstName": "Budi",
"lastName": "Santoso",
"email": "customer@example.com",
"phoneNumber": "081234567890",
"billingAddress": {
"firstName": "Budi",
"lastName": "Santoso",
"address": "Jl. Sudirman No. 45",
"city": "Jakarta",
"postalCode": "12190",
"phone": "081234567890",
"countryCode": "ID"
},
"shippingAddress": {
"firstName": "Budi",
"lastName": "Santoso",
"address": "Jl. Sudirman No. 45",
"city": "Jakarta",
"postalCode": "12190",
"phone": "081234567890",
"countryCode": "ID"
}
}
}'
Success Response (201 Created)
{
"success": true,
"message": "Payment created successfully",
"data": {
"reference": "DK2025112200001",
"payment_url": "https://app-sandbox.duitku.com/payment/DK2025112200001",
"merchant_order_id": "ELFA-22112025-0001",
"webstore_order_id": "WS-2025-001"
}
}
Check Payment Status
/api/payment/status/{webstoreOrderId}
Retrieves the current payment status for a specific transaction using the webstore order ID.
Example Request
curl -X GET https://payment.elfadigital.my.id/api/payment/status/WS-2025-001 \
-H "X-API-Key: EP-BBZZD1PC5OCRBPTE"
Transaction Status Values
| Status | Description |
|---|---|
pending |
Payment is awaiting customer action |
success |
Payment completed successfully |
failed |
Payment failed |
expired |
Payment link expired |
Success Response
{
"success": true,
"data": {
"webstore_order_id": "WS-2025-001",
"merchant_order_id": "ELFA-22112025-0001",
"status": "success",
"payment_amount": 250000,
"duitku_status": {
"merchantCode": "DS12345",
"amount": "250000",
"merchantOrderId": "ELFA-22112025-0001",
"statusCode": "00",
"statusMessage": "SUCCESS"
}
}
}
Duitku Callback
/api/callback/duitku
This endpoint is called by Duitku, not by your application. Duitku sends data as application/x-www-form-urlencoded, NOT JSON.
Duitku Result Codes
| Result Code | Status | Description |
|---|---|---|
00 |
success | Payment successful |
01 |
failed | Payment failed |
02 |
expired | Payment expired |
Callback Data Forwarded to Webstore
| Field | Type | Description |
|---|---|---|
webstore_order_id |
string | Your original order ID |
merchant_order_id |
string | Proxy-generated order ID (ELFA-xxx) |
payment_amount |
integer | Amount in IDR |
status |
string | success/failed/expired/pending |
result_code |
string | Duitku result code (00/01/02) |
duitku_reference |
string | Duitku reference number |
payment_method |
string | Payment method code (BCA, BNI, etc) |
Payment Flow
Webstore → Proxy
Webstore calls POST /api/payment/create with order details
Proxy → Duitku
Proxy generates unique merchant order ID and requests invoice from Duitku
Proxy → Webstore
Proxy returns payment URL to webstore
Customer → Duitku
Customer redirected to payment URL and completes payment
Duitku → Proxy → Webstore
Duitku sends callback to proxy, proxy validates and forwards to webstore
Callback Mechanism
Retry Strategy
When forwarding callbacks to webstore endpoints, the proxy implements an automatic retry mechanism:
| Attempt | Timing | Description |
|---|---|---|
| 1 | Immediate | First attempt right after receiving callback |
| 2 | After 2 seconds | If first attempt fails |
| 3 | After 5 seconds | If second attempt fails |
| 4 | After 10 seconds | If third attempt fails |
Each attempt has a 10-second timeout. Your callback endpoint must respond within this time.
Webstore Callback Handler
- Accept application/x-www-form-urlencoded POST data
- Return HTTP 200 status code for success
- Respond within 10 seconds
- Be idempotent (handle duplicate callbacks)
- Disable CSRF protection for this endpoint
- Use HTTPS in production
PHP/Laravel Example
public function handlePaymentCallback(Request $request)
{
Log::info('Payment callback received', $request->all());
$order = Order::where('id', $request->webstore_order_id)->first();
if (!$order) {
return response()->json(['success' => false], 404);
}
// Idempotency check
if ($order->payment_status === 'success') {
return response()->json(['success' => true, 'message' => 'Already processed']);
}
$order->update([
'payment_status' => $request->status,
'payment_reference' => $request->duitku_reference,
'payment_method' => $request->payment_method,
'paid_at' => $request->status === 'success' ? now() : null,
]);
if ($request->status === 'success') {
SendOrderConfirmationEmail::dispatch($order);
}
return response()->json(['success' => true], 200);
}
Node.js/Express Example
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.post('/callback/payment', async (req, res) => {
const { webstore_order_id, status, duitku_reference } = req.body;
const order = await Order.findOne({ _id: webstore_order_id });
if (!order) {
return res.status(404).json({ success: false });
}
if (order.payment_status === 'success') {
return res.json({ success: true, message: 'Already processed' });
}
order.payment_status = status;
order.payment_reference = duitku_reference;
if (status === 'success') {
order.paid_at = new Date();
}
await order.save();
res.json({ success: true });
});
Python/Django Example
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
@require_POST
def payment_callback(request):
webstore_order_id = request.POST.get('webstore_order_id')
status = request.POST.get('status')
try:
order = Order.objects.get(id=webstore_order_id)
except Order.DoesNotExist:
return JsonResponse({'success': False}, status=404)
if order.payment_status == 'success':
return JsonResponse({'success': True, 'message': 'Already processed'})
order.payment_status = status
if status == 'success':
order.paid_at = timezone.now()
order.save()
return JsonResponse({'success': True})
Error Handling
HTTP Status Codes
| Status | Meaning | When It Occurs |
|---|---|---|
200 |
OK | Request successful |
201 |
Created | Payment created successfully |
401 |
Unauthorized | Missing or invalid API key |
404 |
Not Found | Transaction not found |
409 |
Conflict | Duplicate webstore order ID |
422 |
Unprocessable Entity | Validation error |
500 |
Internal Server Error | Server error or Duitku API error |
Error Response Format
{
"success": false,
"message": "Error description",
"errors": {
"field_name": ["Error message"]
}
}
Security Considerations
API Key Management
- Never expose API keys in client-side code
- Store API keys in environment variables
- Rotate API keys periodically
HTTPS Requirement
- Always use HTTPS in production
- Ensure SSL certificates are valid
- Configure callback URLs with HTTPS
Signature Validation
The proxy validates Duitku callbacks using MD5 signature:
MD5(merchantCode + amount + merchantOrderId + apiKey)
Data Validation
- Validate all input data before processing
- Check amount ranges
- Sanitize string inputs
Testing
Test Create Payment
curl -X POST https://payment.elfadigital.my.id/api/payment/create \
-H "Content-Type: application/json" \
-H "X-API-Key: EP-BBZZD1PC5OCRBPTE" \
-d '{
"webstoreOrderId": "TEST-001",
"paymentAmount": 50000,
"productDetails": "Test Product",
"email": "test@example.com",
"phoneNumber": "081234567890",
"returnUrl": "https://webstore.com/return",
"expiryPeriod": 15,
"itemDetails": [{"name": "Test Product", "price": 50000, "quantity": 1}],
"customerDetail": {
"firstName": "Test",
"lastName": "User",
"email": "test@example.com",
"phoneNumber": "081234567890",
"billingAddress": {...},
"shippingAddress": {...}
}
}'
Test Check Status
curl -X GET https://payment.elfadigital.my.id/api/payment/status/TEST-001 \
-H "X-API-Key: EP-BBZZD1PC5OCRBPTE"
Test Callback Endpoint
curl -X POST https://yourwebstore.com/callback/payment \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "webstore_order_id=TEST-001" \
-d "merchant_order_id=ELFA-22112025-0001" \
-d "payment_amount=100000" \
-d "status=success" \
-d "result_code=00" \
-d "duitku_reference=DK2025110001" \
-d "payment_method=BCA"