Skip to content

Models

The library ships abstract base models. You subclass them and run your own migrations.

AbstractMpesaTransaction

Field Type Description
id UUID (PK) Internal primary key. Never expose to Safaricom.
transaction_type CharField STK_PUSH, C2B, B2C, or REVERSAL
status CharField PENDING, PROCESSING, SUCCESS, FAILED, TIMEOUT, REVERSED
checkout_request_id CharField (unique, null) STK Push idempotency key
merchant_request_id CharField (null) STK Push cross-reference
conversation_id CharField (unique, null) B2C/Reversal idempotency key
originator_conversation_id CharField (null) B2C/Reversal reference
mpesa_receipt_number CharField (null) M-PESA receipt. Set only on SUCCESS.
phone_number CharField E.164 format: 2547XXXXXXXX
amount DecimalField(12,2) Payment amount in KES. Never float.
account_reference CharField (max 12) Reference tied to order/invoice
transaction_desc CharField (max 13) Human-readable description
result_code IntegerField (null) Safaricom result code. 0 = success.
result_desc TextField (null) Safaricom result description
raw_callback_payload JSONField (null) Full callback body for audit/debug
initiated_at DateTimeField Auto-set at creation
settled_at DateTimeField (null) Set once when reaching a terminal state
idempotency_locked BooleanField Internal fencing token

AbstractMpesaCallbackLog

Every inbound callback — valid, invalid, malformed, duplicate — is logged here before any business logic runs.

Field Type Description
id UUID (PK)
callback_type CharField STK, C2B_VALIDATION, C2B_CONFIRMATION, B2C_RESULT, B2C_TIMEOUT
source_ip GenericIPAddressField Source IP of the callback
raw_body JSONField Full request body, stored before any validation
related_transaction_id UUID (null) Linked after processing
processed BooleanField True after processing completes
error TextField (null) Error message if processing failed
received_at DateTimeField Auto-set at receipt

Example host-app implementation

# myapp/models.py
from django.db import models
from django.conf import settings
from django_mpesa.models import AbstractMpesaTransaction, AbstractMpesaCallbackLog

class MpesaTransaction(AbstractMpesaTransaction):
    # Add your domain-specific fields here
    order = models.ForeignKey(
        "orders.Order",
        on_delete=models.PROTECT,
        null=True, blank=True,
    )
    initiated_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True, blank=True,
    )

    class Meta(AbstractMpesaTransaction.Meta):
        verbose_name = "M-PESA Transaction"

class MpesaCallbackLog(AbstractMpesaCallbackLog):
    class Meta(AbstractMpesaCallbackLog.Meta):
        verbose_name = "M-PESA Callback Log"
# settings.py
MPESA = {
    "TRANSACTION_MODEL": "myapp.MpesaTransaction",
    "CALLBACK_LOG_MODEL": "myapp.MpesaCallbackLog",
}

The library never imports your concrete model directly. All model access goes through django.apps.apps.get_model() using the string you configure. This means you can change your model or app name without touching library code.