Unofficial Django SDK for Credo payment integration
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:
GETbrowser redirects from Credo after payment attemptsPOSTserver-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. Credo's callback, verify, and webhook amount fields (transAmount, debitedAmount, etc.) are handled as the currency's main unit. The *_major properties expose those values as two-decimal Decimal objects:
trans_amount_major— transaction amount in Nairadebited_amount_major— debited amount in Nairatrans_fee_amount_major— fee amount in Nairasettlement_amount_major— settlement amount in Naira
assert_amount() and amount_matches() compare your expected main-unit amount against transAmount by default. They accept float or Decimal — pass Decimal when your model stores DecimalField amounts for lossless comparison. Fees such as transFeeAmount are not used for merchant-side amount validation.
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_transfer_reversed:
Order.objects.filter(credo_trans_ref=event.trans_ref).update(status="reversed")
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_transfer_reversed:
Order.objects.filter(credo_trans_ref=event.trans_ref).update(status="reversed")
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 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.transaction_status # int status code
event.payment_channel # normalised channel string ("MasterCard", "Card", etc.)
event.raw_payload # full raw API/webhook payload dict (for auditing)
event.trans_amount_major # Decimal amount in Naira
event.debited_amount_major # Decimal debited amount in Naira
event.trans_fee_amount_major # Decimal fee in Naira
event.settlement_amount_major # Decimal settlement amount in Naira
WebhookEvent-specific properties:
event.is_settlement # True when event_type == "transaction.settlement.success"
event.is_transfer_reversed # True when event_type == "transaction.transaction.transfer.reverse"
event.metadata # dict from the webhook payload
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.
Multi-Tenant Webhook Verification
When building a B2B SaaS platform where each tenant registers their own Credo account with a unique webhook key, a single global CREDO_WEBHOOK_SECRET_TOKEN is not enough. The correct token depends on which tenant the incoming webhook belongs to — something only knowable after reading the payload.
Both CredoWebhookView (DRF) and DjangoCredoWebhookView expose a get_webhook_token() hook for this. Override it to return the per-tenant token. The payload is already parsed when the method is called, so you can read data.businessRef, data.businessCode, or any other field to identify the tenant and look up their credentials.
Returning None (the default) falls back to CREDO_WEBHOOK_SECRET_TOKEN — the standard single-tenant path is unchanged.
DRF
from credo_pay.views.drf import CredoWebhookView
class FeePaymentWebhookView(CredoWebhookView):
def get_webhook_token(self, request, payload):
ref = payload["data"]["businessRef"]
return PaymentGateway.objects.get(reference=ref).webhook_key
def handle_webhook(self, event):
if event.is_successful:
FeePayment.objects.filter(credo_ref=event.trans_ref).update(status="paid")
elif event.is_failed:
FeePayment.objects.filter(credo_ref=event.trans_ref).update(status="failed")
Traditional Django
from credo_pay.views.django import DjangoCredoWebhookView
class FeePaymentWebhookView(DjangoCredoWebhookView):
def get_webhook_token(self, request, payload):
ref = payload["data"]["businessRef"]
return PaymentGateway.objects.get(reference=ref).webhook_key
def handle_webhook(self, event):
if event.is_successful:
FeePayment.objects.filter(credo_ref=event.trans_ref).update(status="paid")
elif event.is_failed:
FeePayment.objects.filter(credo_ref=event.trans_ref).update(status="failed")
When get_webhook_token() returns a token, the view verifies the signature directly with that token — CredoClient is never instantiated, and CREDO_PUBLIC_KEY / CREDO_SECRET_KEY are not required for the webhook path. This means multi-tenant endpoints work correctly even in environments where global Credo credentials are absent or belong to a different account.
Credo includes data.businessRef, data.businessCode, and data.transRef in every webhook payload. Any of these can be used to look up the right tenant.
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 withSELECT FOR UPDATEinside a transactionterminal_field/terminal_values— skips the handler if the row is already in a terminal state. Requireslock— raisesValueErrorimmediately iflockis not also seton_commit— called(event, locked_object, handler_result)after commit when outcome is"success"; use for emails, notifications, WebSocket broadcasts that must not run on rollbackon_failure_commit— same signature ason_commit, fires when outcome is"failed"(confirmed failure or force-failed fromon_success)result.is_skipped—Truewhen the idempotency guard fired
Force failure from on_success (amount mismatch)
When Credo reports a transaction as successful but your own validation rejects it, raise CredoPaymentError or CredoValidationError inside on_success. Both are caught — the SDK calls on_failure instead, sets outcome to "failed", and fires on_failure_commit.
The most common case is assert_amount(), which raises CredoValidationError internally:
def handle_success(event, payment):
event.assert_amount(payment.amount) # raises CredoValidationError on mismatch
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),
)
You can also raise CredoPaymentError directly for custom conditions:
from credo_pay import CredoPaymentError
def handle_success(event, payment):
if not payment.is_eligible():
raise CredoPaymentError("Payment not eligible — treating as failure")
payment.mark_paid()
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")
# Activate the idempotency guard in sync_from_event()
terminal_status_field = "status"
terminal_status_values = ["paid", "failed"]
# --- required ---
def get_payment_amount(self):
return self.amount
def get_payment_email(self):
return self.email
# --- required for sync_payment_status() and sync_from_event() ---
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, event):
event.assert_amount(self.amount) # CredoValidationError → failure path
self.status = "paid"
self.save(update_fields=["status"])
def on_payment_failed(self, event):
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 webhook (no extra API round-trip):
def handle_webhook(self, event):
order = Order.objects.get(credo_ref=event.trans_ref)
result = order.sync_from_event(event)
if result.is_skipped:
logger.info("Duplicate event ignored for %s", event.trans_ref)
Handling a callback redirect:
def handle_callback(self, callback_data, verify_response):
if callback_data.trans_ref and verify_response:
order = Order.objects.get(credo_ref=callback_data.trans_ref)
order.sync_from_event(verify_response)
return redirect("/payment/result/")
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 URLget_payment_currency()— currency codeget_payment_channels()— payment channelsget_payment_first_name(),get_payment_last_name(),get_payment_phone_number()get_payment_service_code(),get_payment_bank_account()— routed paymentsget_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/")
sync_from_event()
sync_from_event(event) routes a pre-parsed VerifyResponse or WebhookEvent to the appropriate hook under a database transaction. Use it when you already have the event — it avoids an extra Credo API round-trip.
It provides the same guarantees as process_payment_event():
- Row-level
SELECT FOR UPDATElock viaget_payment_lock() - Terminal-state idempotency guard (
terminal_status_field/terminal_status_values) - Hooks run inside
transaction.atomic() - Post-commit hooks fire after commit and are discarded on rollback
Returns a PaymentEventResult:
result = order.sync_from_event(event)
result.is_skipped # True when the idempotency guard fired
result.is_successful # True when outcome == "success"
result.is_failed # True when outcome == "failed"
result.is_pending # True when outcome == "pending"
sync_payment_status() calls Credo's verify API and then delegates to sync_from_event() internally. Use it only when you do not already have a verified event.
Idempotency
Set terminal_status_field and terminal_status_values on the class to prevent double-processing when a callback and a webhook both arrive for the same transaction:
class Order(CredoPaymentMixin, models.Model):
terminal_status_field = "status"
terminal_status_values = ["paid", "failed"]
sync_from_event() acquires a row-level SELECT FOR UPDATE lock and checks the terminal state inside a single atomic transaction. If the row is already in a terminal state, it returns immediately with result.is_skipped == True without calling any hook.
terminal_status_field requires get_payment_lock() to return a non-None lock. The default implementation locks by primary key, which is sufficient for most cases.
Post-commit hooks
Override on_payment_confirmed_commit or on_payment_failed_commit for notifications that must not fire if the database transaction rolls back:
class Order(CredoPaymentMixin, models.Model):
...
def on_payment_confirmed(self, event):
self.status = "paid"
self.save(update_fields=["status"])
def on_payment_confirmed_commit(self, event):
send_confirmation_email(self.email)
def on_payment_failed_commit(self, event):
notify_team(self)
Both hooks receive event (VerifyResponse or WebhookEvent). self inside commit hooks is the locked model row returned from get_payment_lock().
If on_payment_confirmed updated related objects via filter().update(), those changes are not reflected on the in-memory object. Call refresh_from_db() before reading them:
def on_payment_confirmed_commit(self, event):
self.applicant.refresh_from_db()
send_email(self.applicant.email)
Custom lock strategy
get_payment_lock() returns a callable that locks the current row by primary key by default. Override it when you need to lock by a different field or eager-load a related object alongside the lock:
def get_payment_lock(self):
from credo_pay import model_payment_lock
return model_payment_lock(
self.__class__,
lookup_field="credo_trans_ref",
)
Return None to disable locking. Only safe when concurrent delivery of the same event is impossible.
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")
terminal_status_field = "status"
terminal_status_values = ["paid", "failed"]
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, event):
self.status = "paid"
self.save(update_fields=["status"])
Amount verification
Call assert_amount() on the event inside on_payment_confirmed() before fulfilling the order. It raises CredoValidationError on mismatch, which sync_from_event() and process_payment_event() both catch and redirect to the failure path:
def on_payment_confirmed(self, event):
event.assert_amount(self.amount, currency="NGN")
self.status = "paid"
self.save(update_fields=["status"])
Type annotations
PaymentEvent = Union[VerifyResponse, WebhookEvent] is the canonical type for hook parameters. Import it from credo_pay.idempotency when annotating custom handlers:
from credo_pay.idempotency import PaymentEvent
def my_handler(event: PaymentEvent) -> None:
...
AbstractCredoPayment
AbstractCredoPayment is an abstract Django model with all standard payment fields pre-wired. Extend it when you want a complete payment model with zero boilerplate.
Import path — AbstractCredoPayment must be imported directly from credo_pay.mixins, not from credo_pay:
from credo_pay.mixins import AbstractCredoPayment
This is intentional: importing it from the top-level package during Django's app loading phase causes an AppRegistryNotReady error.
Minimal usage
from credo_pay.mixins import AbstractCredoPayment
class Payment(AbstractCredoPayment):
class Meta:
app_label = "payments"
The full payment lifecycle works out of the box with zero method overrides:
# Create and initialize
payment = Payment.objects.create(
email="customer@example.com",
amount=Decimal("15000"),
)
response = payment.create_payment()
return redirect(response.authorization_url)
# Handle a webhook — no extra Credo API call
def handle_webhook(self, event):
payment = Payment.objects.get(credo_reference=event.trans_ref)
payment.sync_from_event(event)
Standard fields
| Field | Type | Description |
|---|---|---|
email |
EmailField |
Payer email (used by create_payment()) |
amount |
DecimalField(12, 2) |
Amount in main units (Naira for NGN) |
currency |
CharField |
ISO currency code (default: "NGN") |
credo_reference |
CharField |
Credo's transRef |
business_reference |
CharField |
Your generated business reference |
status |
CharField |
"pending" → "awaiting_payment" → "paid" / "failed" |
payment_method |
CharField |
Card type or payment method from Credo |
confirmed_at |
DateTimeField |
Timestamp when payment was confirmed |
raw_payload |
JSONField |
Full raw API/webhook payload for auditing |
Idempotency is active by default — terminal_status_field = "status" and terminal_status_values = ["paid", "failed"] are pre-set.
Extending with application logic
Override any hook and call super() to keep the standard field updates alongside your business logic:
class Payment(AbstractCredoPayment):
application = models.OneToOneField(Application, on_delete=models.CASCADE)
class Meta:
app_label = "payments"
def on_payment_confirmed(self, event):
super().on_payment_confirmed(event) # persists status, confirmed_at, raw_payload
self.application.mark_approved()
Post-commit notifications
Override on_payment_confirmed_commit for emails and broadcasts that must not fire on rollback:
class Payment(AbstractCredoPayment):
class Meta:
app_label = "payments"
def on_payment_confirmed_commit(self, event):
send_confirmation_email(self.email)
def on_payment_failed_commit(self, event):
notify_team(self)
Helper methods
AbstractCredoPayment provides helper methods that persist the standard fields. The default hooks call these; you can call them from overridden hooks too:
mark_paid(event)— setsstatus="paid",confirmed_at,payment_method,raw_payloadmark_failed(event)— setsstatus="failed",raw_payloadmark_pending(event)— setsraw_payload
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 |
Queued for settlement |
5 |
Settled |
6 |
Review (flagged for manual review) |
7 |
Declined |
8 |
Failed (aged) |
9 |
Cancelled by customer |
10 |
Cancelled by merchant |
12 |
Account generated, awaiting credit |
13 |
Attempted |
14 |
Initialized |
15 |
Initializing |
Production Checklist
- Set
CREDO_ENVIRONMENT=production. - Use live Credo public and secret keys.
- Set
CREDO_DEFAULT_CALLBACK_URLto your HTTPS callback/webhook endpoint. - Register the same HTTPS endpoint in Credo's webhook settings.
- Set a strong
CREDO_WEBHOOK_SECRET_TOKENin 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_credo_sdk-0.1.13.tar.gz.
File metadata
- Download URL: django_credo_sdk-0.1.13.tar.gz
- Upload date:
- Size: 81.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14397b9e742655931566497d2cc2efac06436379c785d5e6e9277e49f59485a7
|
|
| MD5 |
5f9365966ebe0e6978541b6cd024d41a
|
|
| BLAKE2b-256 |
bc4c5fb189cda0430ecfdfb00338608271743a632ea3b179ca3a877274b6b412
|
Provenance
The following attestation bundles were made for django_credo_sdk-0.1.13.tar.gz:
Publisher:
publish.yml on Onuigb0/django-sdk-for-credo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_credo_sdk-0.1.13.tar.gz -
Subject digest:
14397b9e742655931566497d2cc2efac06436379c785d5e6e9277e49f59485a7 - Sigstore transparency entry: 1438602228
- Sigstore integration time:
-
Permalink:
Onuigb0/django-sdk-for-credo@c11cdb7c7d9e077e2fb463a8193616d82a735ee2 -
Branch / Tag:
refs/tags/v0.1.13 - Owner: https://github.com/Onuigb0
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c11cdb7c7d9e077e2fb463a8193616d82a735ee2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_credo_sdk-0.1.13-py3-none-any.whl.
File metadata
- Download URL: django_credo_sdk-0.1.13-py3-none-any.whl
- Upload date:
- Size: 78.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b33f0aa23df68a51a19966881c7749f35714d670ad84ada13b7e38e937656422
|
|
| MD5 |
cbd8000aad9b2327c9df0e1f180329c0
|
|
| BLAKE2b-256 |
853a5ada3659e24c708f0c2be1e05e672e3b07a3e015b3be4961b87eb2ada3a7
|
Provenance
The following attestation bundles were made for django_credo_sdk-0.1.13-py3-none-any.whl:
Publisher:
publish.yml on Onuigb0/django-sdk-for-credo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_credo_sdk-0.1.13-py3-none-any.whl -
Subject digest:
b33f0aa23df68a51a19966881c7749f35714d670ad84ada13b7e38e937656422 - Sigstore transparency entry: 1438602229
- Sigstore integration time:
-
Permalink:
Onuigb0/django-sdk-for-credo@c11cdb7c7d9e077e2fb463a8193616d82a735ee2 -
Branch / Tag:
refs/tags/v0.1.13 - Owner: https://github.com/Onuigb0
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c11cdb7c7d9e077e2fb463a8193616d82a735ee2 -
Trigger Event:
push
-
Statement type: