Webhook Notifications
Midtrans sends HTTP POST notifications (webhooks) to your server when payment statuses change.
Setup
1. Configure the Notification URL
In your Django settings:
MIDTRANS = {
# ...
"NOTIFICATION_URL": "https://yourdomain.com/midtrans/api/notification/",
}
2. Include URLs
# urls.py
urlpatterns = [
path("midtrans/api/", include("django_midtrans.urls")),
]
The notification endpoint is automatically registered at /midtrans/api/notification/.
Important
The notification endpoint uses AllowAny permission — it does not require authentication. Security is enforced through signature verification.
How It Works
Midtrans Server ──POST──► /midtrans/api/notification/
│
NotificationView
│
NotificationHandler
│
┌─────────────┼──────────────┐
│ │ │
Verify Signature Save Record Fire Signal
Midtrans sends a POST with JSON payload containing
order_id,transaction_status,signature_key, etc.NotificationView receives it and delegates to
NotificationHandler.NotificationHandler:
Verifies the SHA-512 signature
Creates a
MidtransNotificationrecordUpdates the linked
MidtransPaymentstatusFires the appropriate Django signal
Signature Verification
Every notification includes a signature_key computed as:
SHA512(order_id + status_code + gross_amount + server_key)
The handler verifies this automatically using MidtransClient.verify_signature(). If the signature is invalid, the notification is saved with status="invalid_signature" and no signal is fired.
Notification Payload
Example payload from Midtrans:
{
"order_id": "ORDER-001",
"transaction_id": "abc-123-def",
"transaction_status": "settlement",
"payment_type": "bank_transfer",
"fraud_status": "accept",
"status_code": "200",
"gross_amount": "100000.00",
"signature_key": "a1b2c3d4..."
}
Notification Statuses
Each MidtransNotification record tracks its processing status:
Status |
Description |
|---|---|
|
Notification received, not yet processed |
|
Successfully processed and payment updated |
|
Processing failed (see |
|
Duplicate notification (already processed) |
|
Signature verification failed |
Handling Notifications with Signals
The recommended way to react to payment events is through Django signals:
# yourapp/signals.py
from django.dispatch import receiver
from django_midtrans.signals import payment_settled, payment_expired
@receiver(payment_settled)
def handle_payment_settled(sender, payment, notification, payload, **kwargs):
"""Called when a payment is successfully settled."""
order = Order.objects.get(midtrans_payment=payment)
order.status = "paid"
order.save()
send_confirmation_email(order)
@receiver(payment_expired)
def handle_payment_expired(sender, payment, notification, payload, **kwargs):
"""Called when a payment expires."""
order = Order.objects.get(midtrans_payment=payment)
order.status = "expired"
order.save()
# Optionally restore inventory
for item in order.items.all():
item.product.stock += item.quantity
item.product.save()
See the Signals Reference page for a complete list of available signals.
Local Development with ngrok
Midtrans needs a publicly accessible URL for webhooks. Use ngrok during development:
# Start your Django server
python manage.py runserver
# In another terminal, expose it
ngrok http 8000
Then set the ngrok URL in your Midtrans dashboard:
https://abc123.ngrok-free.app/midtrans/api/notification/
Or configure it in settings:
MIDTRANS = {
"NOTIFICATION_URL": "https://abc123.ngrok-free.app/midtrans/api/notification/",
}
Tip
In sandbox mode, you can also use the Midtrans dashboard simulator to manually trigger notifications.
Retry Behavior
Midtrans retries failed webhook deliveries. Your endpoint should:
Return HTTP 200 quickly — even if processing takes time
Handle duplicates — the handler automatically marks them as
duplicateBe idempotent — the same notification may arrive more than once
Troubleshooting
Issue |
Solution |
|---|---|
Notifications not arriving |
Check the notification URL in Midtrans dashboard and your settings |
Invalid signature errors |
Verify |
404 on notification URL |
Ensure |
Behind reverse proxy |
Make sure your proxy passes the POST body correctly |