Preloader

Payment Callbacks & Webhooks

Real-time payment notifications for your application

Real-Time Notifications

Instant payment status updates sent to your server via HTTP callbacks

Automatic Retries

Failed callbacks are automatically retried with exponential backoff (5s → 30s → 2min)

Secure & Reliable

JSON payloads with full payment details for reliable integration

Status Tracking

Monitor callback delivery status in your dashboard

Overview

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.

How Callbacks Work

  1. Customer initiates payment and completes transaction
  2. Payment gateway confirms payment status
  3. Your application receives HTTP POST request at your callback URL
  4. Your application processes the notification and responds with HTTP 200
  5. Callback delivery is logged and marked as successful

Registering Your Callback URL

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())

Callback Payload

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"
}

Payload Fields

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

Processing Callbacks

Your callback endpoint should:

  1. Accept HTTP POST requests with JSON payloads
  2. Parse the JSON and validate the payment details
  3. Update your database with the payment status
  4. Respond with HTTP 200 OK status (empty body is fine)
  5. Handle retries gracefully (idempotency)
// 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)

Automatic Retry Logic

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

⚠️ Idempotency is Critical

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!

Best Practices

✅ Recommended Practices

  • Use HTTPS: Always use HTTPS URLs for callbacks to ensure data security
  • Idempotent Processing: Check if payment is already processed before updating
  • Fast Response: Return HTTP 200 quickly; do heavy processing in background jobs
  • Logging: Log all callback requests for audit and debugging purposes
  • Error Handling: Always return HTTP 200 except for invalid request formats
  • Verify Data: Validate amount and order_id before processing
  • Queue Processing: Use background jobs for sending emails and notifications
  • Monitoring: Set up alerts for failed callbacks or payment discrepancies

Monitoring Callbacks

All callback deliveries are logged in your account. You can view:

  • Callback status (PENDING, SUCCESS, FAILED)
  • Number of retry attempts
  • Response status code from your endpoint
  • Timestamp of each attempt
  • Full request and response logs

Monitoring Your Endpoint

We recommend implementing these monitoring practices:

  • Log all incoming callbacks with timestamps
  • Track callback processing success/failure rates
  • Set up alerts for processing errors
  • Periodically verify payment status matches your records
  • Monitor endpoint response times

Testing Callbacks

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.

Using the Payment Simulator

Visit Payment Simulator to:

  • Create test QRIS payments
  • Simulate payment completion
  • Trigger callbacks to your endpoint
  • Monitor delivery status
  • Test error scenarios

Troubleshooting

  • Verify callback URL is correct and publicly accessible
  • Check that your endpoint returns HTTP 200 OK
  • Ensure your firewall allows requests from our servers
  • Check server logs for incoming requests
  • Verify DNS resolution for your domain
  • Check callback status in your dashboard

  • Ensure your endpoint returns HTTP 200 immediately
  • Don't perform blocking I/O in callback handler
  • Check for runtime errors or exceptions
  • Verify request timeout settings are appropriate
  • Use background jobs for long-running operations

  • Always check if payment is already processed before updating
  • Use database transactions for atomic operations
  • Implement unique constraints on order_id or transaction_id
  • Log all callback processing for audit
  • Use idempotency keys for sensitive operations

Need Help?

For additional support with callbacks, please visit our Support Page or contact our development team.