Real-time payment notifications for your application
Instant payment status updates sent to your server via HTTP callbacks
Failed callbacks are automatically retried with exponential backoff (5s → 30s → 2min)
JSON payloads with full payment details for reliable integration
Monitor callback delivery status in your dashboard
When a customer completes a payment through our payment gateway, we send a real-time notification (callback) to your registered endpoint. This allows your application to instantly update payment status, trigger order processing, and provide immediate user feedback.
To receive payment callbacks, include your callback URL when initiating a payment. Here's how:
curl -X POST https://cicis.online/api/qris/initiate \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"order_id": "ORDER-12345",
"customer_name": "John Doe",
"customer_email": "john@example.com",
"customer_phone": "+62812345678",
"callback_url": "https://yourdomain.com/payment-callback"
}'
const axios = require('axios');
const paymentData = {
amount: 50000,
order_id: 'ORDER-12345',
customer_name: 'John Doe',
customer_email: 'john@example.com',
customer_phone: '+62812345678',
callback_url: 'https://yourdomain.com/payment-callback'
};
axios.post('https://cicis.online/api/qris/initiate', paymentData, {
headers: {
'Authorization': `Bearer YOUR_ACCESS_TOKEN`,
'Content-Type': 'application/json'
}
})
.then(response => {
console.log('Payment initiated:', response.data);
})
.catch(error => {
console.error('Error:', error.response.data);
});
$curl = curl_init();
$payload = json_encode([
'amount' => 50000,
'order_id' => 'ORDER-12345',
'customer_name' => 'John Doe',
'customer_email' => 'john@example.com',
'customer_phone' => '+62812345678',
'callback_url' => 'https://yourdomain.com/payment-callback'
]);
curl_setopt_array($curl, [
CURLOPT_URL => 'https://cicis.online/api/qris/initiate',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_ACCESS_TOKEN',
'Content-Type: application/json'
]
]);
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
echo json_decode($response, true);
import requests
url = 'https://cicis.online/api/qris/initiate'
payload = {
'amount': 50000,
'order_id': 'ORDER-12345',
'customer_name': 'John Doe',
'customer_email': 'john@example.com',
'customer_phone': '+62812345678',
'callback_url': 'https://yourdomain.com/payment-callback'
}
headers = {
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/json'
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
When payment is completed, we'll send an HTTP POST request to your callback URL with the following payload:
{
"order_id": "ORDER-12345",
"transaction_id": "TXN-20240120-001",
"status": "SUCCESS",
"amount": 50000,
"currency": "IDR",
"payment_method": "QRIS",
"customer_name": "John Doe",
"customer_email": "john@example.com",
"customer_phone": "+62812345678",
"timestamp": "2024-01-20T10:30:45Z",
"description": "Payment for Order #12345"
}
| Field | Type | Description |
|---|---|---|
order_id |
string | Your order ID from the payment initiation |
transaction_id |
string | Unique payment gateway transaction ID |
status |
string | Payment status: SUCCESS or FAILED |
amount |
number | Payment amount in the specified currency |
currency |
string | Payment currency (IDR) |
payment_method |
string | Payment method used (QRIS, VA, etc) |
customer_name |
string | Customer name |
customer_email |
string | Customer email address |
customer_phone |
string | Customer phone number |
timestamp |
string | ISO 8601 timestamp when payment was completed |
description |
string | Payment description |
Your callback endpoint should:
// routes/web.php
Route::post('/payment-callback', [PaymentController::class, 'handleCallback']);
// app/Http/Controllers/PaymentController.php
namespace App\Http\Controllers;
use App\Models\Order;
use Illuminate\Http\Request;
class PaymentController extends Controller
{
public function handleCallback(Request $request)
{
try {
// Parse incoming JSON
$payload = $request->json()->all();
// Validate required fields
$request->validate([
'order_id' => 'required|string',
'transaction_id' => 'required|string',
'status' => 'required|in:SUCCESS,FAILED',
'amount' => 'required|numeric'
]);
// Find order (idempotency check)
$order = Order::where('order_id', $payload['order_id'])->first();
if (!$order) {
return response()->json(['error' => 'Order not found'], 404);
}
// Check if already processed (prevent duplicate processing)
if ($order->payment_status === $payload['status']) {
return response('OK', 200);
}
// Update order status
$order->update([
'payment_status' => $payload['status'],
'transaction_id' => $payload['transaction_id'],
'payment_date' => now()
]);
// Handle successful payment
if ($payload['status'] === 'SUCCESS') {
// Send confirmation email
Mail::send(new PaymentConfirmation($order));
// Trigger order processing
ProcessOrder::dispatch($order);
}
// Return 200 OK
return response('OK', 200);
} catch (\Exception $e) {
\Log::error('Callback processing error: ' . $e->getMessage());
// Still return 200 to prevent retries for invalid requests
return response('OK', 200);
}
}
}
// routes/payment.js
const express = require('express');
const router = express.Router();
const PaymentController = require('../controllers/PaymentController');
router.post('/callback', PaymentController.handleCallback);
module.exports = router;
// controllers/PaymentController.js
const Order = require('../models/Order');
exports.handleCallback = async (req, res) => {
try {
const payload = req.body;
// Validate required fields
if (!payload.order_id || !payload.status || !payload.amount) {
return res.status(400).json({ error: 'Missing required fields' });
}
// Find order (idempotency check)
let order = await Order.findOne({ order_id: payload.order_id });
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
// Check if already processed
if (order.payment_status === payload.status) {
return res.status(200).send('OK');
}
// Update order
order.payment_status = payload.status;
order.transaction_id = payload.transaction_id;
order.payment_date = new Date();
await order.save();
// Handle successful payment
if (payload.status === 'SUCCESS') {
// Send confirmation
sendPaymentConfirmation(order);
// Process order
processOrder(order);
}
res.status(200).send('OK');
} catch (error) {
console.error('Callback error:', error);
// Return 200 to prevent retries
res.status(200).send('OK');
}
};
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('payment-callback/', views.payment_callback, name='payment_callback'),
]
# views.py
from django.http import HttpResponse
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt
import json
from .models import Order
from .tasks import process_order
@csrf_exempt
@require_http_methods(["POST"])
def payment_callback(request):
try:
# Parse JSON payload
payload = json.loads(request.body)
# Validate required fields
required_fields = ['order_id', 'status', 'amount']
if not all(field in payload for field in required_fields):
return HttpResponse('OK', status=200)
# Find order
try:
order = Order.objects.get(order_id=payload['order_id'])
except Order.DoesNotExist:
return HttpResponse('OK', status=200)
# Check if already processed (idempotency)
if order.payment_status == payload['status']:
return HttpResponse('OK', status=200)
# Update order
order.payment_status = payload['status']
order.transaction_id = payload['transaction_id']
order.payment_date = timezone.now()
order.save()
# Handle successful payment
if payload['status'] == 'SUCCESS':
send_payment_confirmation(order)
process_order.delay(order.id)
return HttpResponse('OK', status=200)
except Exception as e:
print(f'Callback error: {str(e)}')
return HttpResponse('OK', status=200)
If your endpoint doesn't respond with HTTP 200, we automatically retry the callback with exponential backoff:
| Attempt | Delay After Failure | Description |
|---|---|---|
| 1 | 5 seconds | First retry for temporary network issues |
| 2 | 30 seconds | Second retry with longer backoff |
| 3 | 2 minutes | Final retry before permanent failure |
Because callbacks are retried, your endpoint must be idempotent.
This means processing the same callback multiple times should have the same result as processing it once.
Always check if the payment has already been processed before updating your database!
All callback deliveries are logged in your account. You can view:
We recommend implementing these monitoring practices:
To test your callback implementation, you can use our Payment Simulator which allows you to simulate payment completion and trigger real callbacks to your registered endpoint.
Visit Payment Simulator to:
For additional support with callbacks, please visit our Support Page or contact our development team.