Skip to main content

Unofficial Django SDK for Credo payment integration

Reason this release was yanked:

too buggy

Project description

Django Credo SDK

Unofficial Django SDK for integrating Credo Payment Gateway into Django applications.

The SDK gives you a small, server-side integration surface for:

  • initializing Credo payment sessions
  • generating readable unique payment references
  • verifying transactions before fulfillment
  • handling Credo browser callbacks and server webhooks on one endpoint
  • verifying webhook signatures
  • processing payment events idempotently
  • using optional Django model mixins for common payment workflows
  • testing your configuration from a built-in debug page

Installation

Traditional Django (no DRF)

pip install django-credo-sdk

Django REST Framework projects

pip install "django-credo-sdk[drf]"

Requirements

  • Python 3.10+
  • Django 5.0+
  • requests 2.31+
  • Django REST Framework 3.14+ (optional — only needed for DRF views and endpoints)

The package does not cap Django below future major versions, so supported environments can resolve newer Django releases such as Django 6 when your Python version supports them.

Django Setup

Traditional Django (no DRF)

# settings.py
INSTALLED_APPS = [
    # ...
    "credo_pay",   # required — enables template and static file discovery
]
# urls.py
from django.urls import include, path

urlpatterns = [
    path("api/credo/", include("credo_pay.urls")),
]

The callback-webhook/ endpoint is always available. The debug interface is also available. Payment API endpoints (initialize/, verify/, routed/initialize/) are only registered when DRF is installed.

DRF projects

# settings.py
INSTALLED_APPS = [
    # ...
    "rest_framework",
    "credo_pay",
]
# urls.py
from django.urls import include, path

urlpatterns = [
    path("api/credo/", include("credo_pay.urls")),
]

Configure Credo from Django settings or environment variables:

# settings.py
CREDO_PUBLIC_KEY = "your_public_key"
CREDO_SECRET_KEY = "your_secret_key"
CREDO_ENVIRONMENT = "sandbox"  # "sandbox" or "production"
CREDO_DEFAULT_CALLBACK_URL = "https://yoursite.com/api/credo/callback-webhook/"

# Optional, but recommended for webhooks
CREDO_WEBHOOK_SECRET_TOKEN = "token-you-set-in-credo-dashboard"

# Optional defaults
CREDO_DEFAULT_CURRENCY = "NGN"
CREDO_DEFAULT_CHANNELS = ["card", "bank"]
CREDO_DEFAULT_BEARER = 0  # 0 = customer pays fee, 1 = merchant pays fee
CREDO_LOGO_URL = ""       # Credo expects a 60x60 logo
CREDO_REQUEST_TIMEOUT = 15

If you use python-decouple, the same settings can be loaded from .env.

CREDO_PUBLIC_KEY=your_public_key
CREDO_SECRET_KEY=your_secret_key
CREDO_ENVIRONMENT=sandbox
CREDO_DEFAULT_CALLBACK_URL=https://yoursite.com/api/credo/callback-webhook/
CREDO_WEBHOOK_SECRET_TOKEN=your_webhook_token
CREDO_REQUEST_TIMEOUT=15

CREDO_DEFAULT_CALLBACK_URL should point to the SDK's unified callback/webhook endpoint. The same URL handles:

  • GET browser redirects from Credo after payment attempts
  • POST server-to-server webhook notifications from Credo

In production, callback and logo URLs must use HTTPS. Local http://localhost callback URLs are only for sandbox browser-redirect testing.

Terminal ID

Credo Terminal IDs identify merchant accounts on Credo's platform. The current Credo initialize, verify, callback, and webhook endpoints used by this SDK do not accept a terminalId field, so CREDO_TERMINAL_ID is not required by this package.

Keep your Terminal ID with your Credo credentials, but do not configure it for this SDK unless Credo adds an endpoint requirement later.

Environment URLs

The SDK chooses Credo's API URL from CREDO_ENVIRONMENT.

Environment API URL
sandbox https://api.credodemo.com
production https://api.credocentral.com

Amounts

Credo expects payment initialization amounts in the lowest currency denomination. For NGN, that is kobo.

The SDK public APIs accept the currency's main unit instead:

client.initialize_payment(
    amount=15000,  # NGN 15,000
    email="customer@example.com",
)

The SDK sends 1500000 to Credo for NGN. Verification and webhook payloads retain Credo's raw amount fields (in kobo) and also expose main-unit helpers:

  • trans_amount_major — transaction amount in Naira
  • debited_amount_major — debited amount in Naira
  • trans_fee_amount_major — fee amount in Naira
  • settlement_amount_major — settlement amount in Naira

assert_amount() and amount_matches() accept float or Decimal — pass Decimal when your model stores DecimalField amounts for lossless comparison.

Initialize a Payment

from credo_pay import CredoClient

client = CredoClient()

response = client.initialize_payment(
    amount=15000,  # NGN 15,000; SDK sends kobo to Credo
    email="customer@example.com",
    callback_url="https://yoursite.com/api/credo/callback-webhook/",
    first_name="John",
    last_name="Doe",
    phone_number="2348032132100",
    reference_prefix="ORDER",
)

redirect_url = response.authorization_url
trans_ref = response.trans_ref
business_reference = response.reference

Redirect the customer to response.authorization_url.

Payment References

Credo references must be unique and alphanumeric. If you want references that carry product or order context, pass reference_prefix:

response = client.initialize_payment(
    amount=15000,
    email="customer@example.com",
    reference_prefix="codex",
)

You can also generate a reference yourself:

from credo_pay import generate_payment_reference

reference = generate_payment_reference(prefix="codex")

response = client.initialize_payment(
    amount=15000,
    email="customer@example.com",
    reference=reference,
)

The prefix is sanitized to alphanumeric characters because Credo references do not support spaces, hyphens, or symbols. Store both your business reference and Credo's trans_ref for reconciliation.

Verify Before Fulfillment

Credo's browser callback is a redirect with query parameters. Treat it as a signal to verify, not as proof of payment.

verification = client.verify_payment("jIMj005d3Y100M0A00MB")

if verification.is_successful:
    verification.assert_amount(15000, currency="NGN")
    fulfill_order()
elif verification.is_failed:
    mark_order_failed()
elif verification.is_review:
    flag_for_manual_review()
else:
    keep_order_pending()

assert_amount() receives the expected amount in the currency's main unit, not kobo. It accepts float or Decimal.

Unified Callback/Webhook View

The SDK's built-in URL is:

/api/credo/callback-webhook/

It is intentionally named callback-webhook because it serves two Credo flows:

Method Source Purpose
GET Customer browser redirect Parse callback query params and verify transaction
POST Credo server webhook Verify signature, parse event, run handlers

Traditional Django (no DRF)

Subclass DjangoCredoWebhookView from credo_pay.views.django:

from django.shortcuts import redirect
from credo_pay.views.django import DjangoCredoWebhookView


class MyCredoView(DjangoCredoWebhookView):

    def handle_callback(self, callback_data, verify_response):
        if verify_response and verify_response.is_successful:
            Order.objects.filter(reference=callback_data.reference).update(status="paid")
            return redirect("/payment/success/")
        return redirect("/payment/failed/")

    def handle_webhook(self, event):
        if event.is_successful:
            Order.objects.filter(credo_trans_ref=event.trans_ref).update(status="paid")
        elif event.is_failed:
            Order.objects.filter(credo_trans_ref=event.trans_ref).update(status="failed")
        elif event.is_settlement:
            Transaction.objects.filter(credo_trans_ref=event.trans_ref).update(settled=True)
# urls.py
from django.urls import path
from .views import MyCredoView

urlpatterns = [
    path("api/credo/callback-webhook/", MyCredoView.as_view(), name="credo-callback-webhook"),
]

DRF projects

Subclass CredoWebhookView from credo_pay.views.drf:

from django.shortcuts import redirect
from credo_pay.views.drf import CredoWebhookView


class MyCredoView(CredoWebhookView):

    def handle_callback(self, callback_data, verify_response):
        if verify_response and verify_response.is_successful:
            Order.objects.filter(reference=callback_data.reference).update(status="paid")
            return redirect("/payment/success/")
        return redirect("/payment/failed/")

    def handle_webhook(self, event):
        if event.is_successful:
            Order.objects.filter(credo_trans_ref=event.trans_ref).update(status="paid")
        elif event.is_failed:
            Order.objects.filter(credo_trans_ref=event.trans_ref).update(status="failed")
        elif event.is_settlement:
            Transaction.objects.filter(credo_trans_ref=event.trans_ref).update(settled=True)

handle_callback() should return an HttpResponse (or redirect) to override the default response, or None to fall through to the SDK's built-in JSON response.

verify_response is None when verify_callback_transaction=False or when trans_ref was absent from Credo's redirect. Always check before using it.

Webhook Event Properties

WebhookEvent and VerifyResponse share a consistent set of status properties:

event.is_successful   # True for status 0, 4, 5
event.is_failed       # True for status 3, 7, 8, 9
event.is_pending      # True for status 13, 14, 15, 6
event.is_review       # True for status 6 (flagged for human review)
event.is_settlement   # True when event == "transaction.settlement.success"

WebhookEvent-specific properties:

event.transaction_status   # int status code (mirrors VerifyResponse.transaction_status)
event.payment_channel      # normalised channel string ("MasterCard", "Card", etc.)
event.metadata             # dict from the webhook payload
event.trans_amount_major   # Decimal amount in Naira
event.debited_amount_major # Decimal debited amount in Naira

Webhook Signatures

Credo signs webhook requests with a SHA512 hash:

SHA512(webhook_token + business_code)

Set CREDO_WEBHOOK_SECRET_TOKEN to the token configured in Credo's dashboard. Credo sends data.businessCode in each webhook payload, and the SDK uses that payload value for signature verification.

Idempotent Payment Processing

Callbacks and webhooks may arrive close together. Use process_payment_event() with a row lock so fulfillment happens exactly once.

from credo_pay import model_payment_lock, process_payment_event

result = process_payment_event(
    reference=event.trans_ref,
    event=event,
    lock=model_payment_lock(Order, lookup_field="credo_trans_ref"),
    terminal_field="status",
    terminal_values=["paid", "failed"],
    on_success=lambda event, order: order.mark_paid(),
    on_failure=lambda event, order: order.mark_failed(),
    on_commit=lambda event, order, _: send_confirmation_email(order),
    on_failure_commit=lambda event, order, _: send_failure_notification(order),
)

if result.is_skipped:
    pass  # already processed — idempotency guard fired
  • lock — locks the matching row with SELECT FOR UPDATE inside a transaction
  • terminal_field / terminal_values — skips the handler if the row is already in a terminal state. Requires lock — raises ValueError immediately if lock is not also set
  • on_commit — called (event, locked_object, handler_result) after commit when outcome is "success"; use for emails, notifications, WebSocket broadcasts that must not run on rollback
  • on_failure_commit — same signature as on_commit, fires when outcome is "failed" (confirmed failure or force-failed from on_success)
  • result.is_skippedTrue when the idempotency guard fired

Force failure from on_success (amount mismatch)

When Credo reports a transaction as successful but your own validation rejects it (e.g. the verified amount does not match), raise CredoPaymentError inside on_success. The SDK catches it, calls on_failure instead, sets outcome to "failed", and fires on_failure_commit.

from credo_pay import CredoPaymentError

def handle_success(event, payment):
    if not event.amount_matches(payment.amount):
        raise CredoPaymentError("Amount mismatch — treating as failure")
    payment.mark_paid()

process_payment_event(
    ...
    on_success=handle_success,
    on_failure=lambda event, payment: payment.mark_failed(),
    on_failure_commit=lambda event, payment, _: notify_team_of_mismatch(payment),
)

Stale related objects in on_commit

locked_object reflects the database state at the time of the lock query. If on_success updated related rows via filter().update(), those changes are not reflected on the in-memory Python object. Call refresh_from_db() inside on_commit before reading related objects:

def send_confirmation(event, payment, _result):
    payment.applicant.refresh_from_db()   # filter().update() does not update in-memory objects
    send_email(payment.applicant.email, subject="Payment confirmed")

Custom OR lookups

When a webhook carries trans_ref and your callback carries business_ref, pass a callable to lookup_field:

from django.db.models import Q

lock = model_payment_lock(
    Payment,
    lookup_field=lambda model, ref: model.objects.filter(
        Q(credo_trans_ref=ref) | Q(business_reference=ref)
    ),
)

Controlling the row lock scope

When lookup_field is a callable that calls select_related(), the default SELECT FOR UPDATE locks all joined tables. Pass select_for_update_kwargs to limit the lock to the payment row only:

lock = model_payment_lock(
    Payment,
    lookup_field=lambda m, ref: m.objects.select_related("application").filter(pk=ref),
    select_for_update_kwargs={"of": ("self",)},
)

Model Mixin

CredoPaymentMixin is optional. It reduces boilerplate for the common pattern of keeping payment state on a Django model.

The mixin handles only Credo API calls. You own your model's fields and decide what to persist in each lifecycle hook.

from django.db import models
from credo_pay.mixins import CredoPaymentMixin


class Order(CredoPaymentMixin, models.Model):
    amount    = models.DecimalField(max_digits=12, decimal_places=2)
    email     = models.EmailField()
    credo_ref = models.CharField(max_length=64, blank=True)
    status    = models.CharField(max_length=20, default="pending")

    # --- required ---

    def get_payment_amount(self):
        return self.amount

    def get_payment_email(self):
        return self.email

    # --- required for sync_payment_status() ---

    def get_credo_trans_ref(self):
        return self.credo_ref or None

    # --- hooks: override only what you need ---

    def on_payment_initiated(self, response):
        self.credo_ref = response.trans_ref or ""
        self.status = "awaiting_payment"
        self.save(update_fields=["credo_ref", "status"])

    def on_payment_confirmed(self, verification):
        self.status = "paid"
        self.save(update_fields=["status"])

    def on_payment_failed(self, verification):
        self.status = "failed"
        self.save(update_fields=["status"])

Starting a payment in a view:

order = get_object_or_404(Order, pk=pk)
response = order.create_payment()
return redirect(response.authorization_url)

Handling a callback or webhook:

order = Order.objects.get(credo_ref=event.trans_ref)
order.sync_payment_status()

Two methods are required: get_payment_amount() and get_payment_email(). Override get_credo_trans_ref() so that sync_payment_status() can locate the right Credo transaction. Everything else returns None by default and can be overridden when needed:

  • get_payment_callback_url() — per-object callback URL
  • get_payment_currency() — currency code
  • get_payment_channels() — payment channels
  • get_payment_first_name(), get_payment_last_name(), get_payment_phone_number()
  • get_payment_service_code(), get_payment_bank_account() — routed payments
  • get_payment_metadata(), get_payment_custom_fields()
  • get_payment_bearer() — who pays the transaction fee (0 = customer, 1 = merchant)

All keyword arguments accepted by CredoClient.initialize_payment() can also be passed directly to create_payment() as one-off overrides without subclassing:

response = order.create_payment(callback_url="https://yoursite.com/api/credo/callback-webhook/")

Routed payments with the mixin

Override get_payment_service_code() and get_payment_bank_account():

class VendorOrder(CredoPaymentMixin, models.Model):
    amount       = models.DecimalField(max_digits=12, decimal_places=2)
    email        = models.EmailField()
    service_code = models.CharField(max_length=80)
    bank_account = models.CharField(max_length=20)
    credo_ref    = models.CharField(max_length=64, blank=True)
    status       = models.CharField(max_length=20, default="pending")

    def get_payment_amount(self):        return self.amount
    def get_payment_email(self):         return self.email
    def get_credo_trans_ref(self):       return self.credo_ref or None
    def get_payment_service_code(self):  return self.service_code
    def get_payment_bank_account(self):  return self.bank_account

    def on_payment_initiated(self, response):
        self.credo_ref = response.trans_ref or ""
        self.status = "pending"
        self.save(update_fields=["credo_ref", "status"])

    def on_payment_confirmed(self, verification):
        self.status = "paid"
        self.save(update_fields=["status"])

Preventing duplicate processing

When a callback and a webhook arrive at the same time, use payment_lock() inside your hook:

def on_payment_confirmed(self, verification):
    with self.payment_lock() as locked:
        if locked.status == "paid":
            return                      # already processed, do nothing
        locked.status = "paid"
        locked.save(update_fields=["status"])

Amount verification

Call assert_amount() on the VerifyResponse inside on_payment_confirmed() before fulfilling the order:

def on_payment_confirmed(self, verification):
    verification.assert_amount(self.amount, currency="NGN")
    self.status = "paid"
    self.save(update_fields=["status"])

Routed Payments

Use routed payments when Credo has configured dynamic settlement service codes for your account.

response = client.initialize_routed_payment(
    amount=15000,
    email="customer@example.com",
    service_code="YOUR_SERVICE_CODE",
    bank_account="0114877128",
    callback_url="https://yoursite.com/api/credo/callback-webhook/",
)

For model-based routed payments, use CredoPaymentMixin and override get_payment_service_code() and get_payment_bank_account(). See the Routed payments with the mixin example in the Model Mixin section above.

REST Endpoints

When DRF is installed and credo_pay.urls is included under /api/credo/, the SDK provides:

Endpoint Method Auth Description
/api/credo/initialize/ POST Open Initialize a payment (DRF only)
/api/credo/routed/initialize/ POST Open Initialize a routed payment (DRF only)
/api/credo/verify/<trans_ref>/ GET Open Verify a transaction (DRF only)
/api/credo/callback-webhook/ GET Open Customer redirect callback
/api/credo/callback-webhook/ POST Open Credo server webhook
/api/credo/debug/ GET Debug-gated Development debug interface

DRF endpoints default to AllowAny. If your application requires users to be logged in before initiating a payment, subclass the view and set your own permission_classes:

from rest_framework.permissions import IsAuthenticated
from credo_pay.views.drf import PaymentInitializeView

class AuthenticatedPaymentView(PaymentInitializeView):
    permission_classes = [IsAuthenticated]

Webhook and callback endpoints must remain open because Credo's servers cannot supply Django authentication credentials. Authenticity of POST webhooks is enforced through X-Credo-Signature verification instead.

Debug Interface

Visit:

http://localhost:8000/api/credo/debug/

The debug interface is available only when Django DEBUG=True or CREDO_ENABLE_DEBUG_INTERFACE=True. Do not enable it in production. It requires no DRF.

The debug interface uses the same configured SDK settings as normal payment calls. It does not invent callback URLs. If CREDO_DEFAULT_CALLBACK_URL is missing, the configuration check fails.

Debug payment initialization uses CREDO_DEBUG_REQUEST_TIMEOUT when set, defaulting to 8 seconds. Normal SDK calls use CREDO_REQUEST_TIMEOUT, defaulting to 15 seconds.

Use it to:

  • confirm keys, environment, API URL, callback URL, and webhook token state
  • initialize a sandbox payment with an optional callback URL override and reference prefix
  • see the generated reference (with your prefix applied) next to the trans_ref
  • verify a transaction by Credo trans_ref
  • watch real callbacks and webhooks that reach the SDK endpoint
  • copy integration examples

Local browser callbacks can work with localhost because the customer browser is redirected. Server-to-server webhooks from Credo need a public HTTPS URL, for example through a tunnel, and that public URL must be registered in Credo's dashboard.

The debug webhook inbox starts polling only after you initialize a debug payment and stops when a callback or webhook arrives.

Status Codes

Code Meaning
0 Successful
1 Refunded
2 Refund queued
3 Failed
4 Successful, queued for settlement
5 Settled
6 Review
7 Declined
8 Failed aged
9 Abandoned
13 Attempted
14 Initialised
15 Initialising

Production Checklist

  • Set CREDO_ENVIRONMENT=production.
  • Use live Credo public and secret keys.
  • Set CREDO_DEFAULT_CALLBACK_URL to your HTTPS callback/webhook endpoint.
  • Register the same HTTPS endpoint in Credo's webhook settings.
  • Set a strong CREDO_WEBHOOK_SECRET_TOKEN in both Credo and Django.
  • Always verify payment status before fulfillment.
  • Make order fulfillment idempotent.
  • Do not expose secret keys in frontend code.

Support

  • Credo support: hello@credocentral.com
  • Sandbox dashboard: https://www.credodemo.com
  • Production dashboard: https://www.credocentral.com

Disclaimer

This is an unofficial SDK and is not affiliated with or endorsed by Credo.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_credo_sdk-0.1.9.tar.gz (65.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_credo_sdk-0.1.9-py3-none-any.whl (72.8 kB view details)

Uploaded Python 3

File details

Details for the file django_credo_sdk-0.1.9.tar.gz.

File metadata

  • Download URL: django_credo_sdk-0.1.9.tar.gz
  • Upload date:
  • Size: 65.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for django_credo_sdk-0.1.9.tar.gz
Algorithm Hash digest
SHA256 57e8f87df474e382f463956d48d1154a46c0df781e8eea41ca781cf621a7050a
MD5 792613e8fcedfe27e8d7480e270431b4
BLAKE2b-256 293aa5ad2464c3d343251a01a4c9490655d7a42714b4d32f13a6c1e679229d73

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_credo_sdk-0.1.9.tar.gz:

Publisher: publish.yml on Onuigb0/django-sdk-for-credo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_credo_sdk-0.1.9-py3-none-any.whl.

File metadata

File hashes

Hashes for django_credo_sdk-0.1.9-py3-none-any.whl
Algorithm Hash digest
SHA256 51a9bae567f7e8a78c65470ad6346c529424e16e887f5f5d502778bb85342224
MD5 ed95e1f14dd4069a0c6339e55afe7d5e
BLAKE2b-256 b53d45dc700d3b65a598831b1d21ab68cc1e0987b0c6dcfab74b29f0d1b456aa

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_credo_sdk-0.1.9-py3-none-any.whl:

Publisher: publish.yml on Onuigb0/django-sdk-for-credo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page