Python SDK
The official Credicorp client for Python. It handles OAuth 2.0 token caching, automatic retries with idempotency, cursor pagination and webhook signature verification — so you write business logic, not plumbing.
Requirements. Python 3.9+. Fully type-hinted (ships py.typed), so editors and mypy understand every model. A synchronous Credicorp client and an AsyncCredicorp client share the same surface — await the methods on the async one.
Install
Install from PyPI — ideally inside a virtual environment. The package depends only on httpx for transport.
python -m pip install credicorp # or, with uv: uv add credicorp
Configure from the environment
Never hard-code secrets. Read your key from the environment — the client selects sandbox or live automatically from the key prefix (sk_test_… vs sk_live_…), so the same code runs in both without a branch. If you omit api_key entirely, the client reads CC_SECRET_KEY for you.
# exported into the process environment
CC_SECRET_KEY=sk_test_9fK2c…
CC_WEBHOOK_SECRET=whsec_4Td1…import os from credicorp import Credicorp cc = Credicorp( api_key=os.environ["CC_SECRET_KEY"], max_retries=3, # retries 429 / 5xx with backoff timeout=30.0, # seconds )
The constructor also accepts api_base= to pin a host, and a client_id/client_secret pair instead of a secret key when calling the partner plane for multiple merchants. Tokens are minted lazily, cached and refreshed before expiry — you never touch /oauth/token directly.
Constructor options
| Argument | Type | Description | |
|---|---|---|---|
api_key | str | opt | Secret key. Falls back to CC_SECRET_KEY if not passed. |
max_retries | int | opt | Retries on 429 and 5xx with exponential backoff; honours Retry-After. Default 3. |
timeout | float | opt | Per-request timeout in seconds. Default 30.0. |
api_base | str | opt | Override the API host. Defaults to https://api.credicorp.co.uk. |
Your first call — create an application
This creates a business-loan application for a UK incorporated company and returns a handoff_url you redirect the applicant to. Amounts are always in pence to avoid floating-point rounding. Pass idempotency_key= (an order id, say) so retries never double-submit.
application = cc.applications.create(
business={"company_number": "16093826"},
amount_pence=2_500_000, # £25,000
term_months=12,
purpose="working_capital",
redirect_uri="https://yourapp.com/return",
idempotency_key="order_88421",
)
print(application.id) # 'app_8Kd2c9Qm'
print(application.handoff_url) # hosted apply journeyThat is exactly one authenticated HTTP request. The equivalent in raw curl:
curl -s https://api.credicorp.co.uk/partner/v1/applications \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: order_88421" \ -H "Content-Type: application/json" \ -d '{ "business": { "company_number": "16093826" }, "amount_pence": 2500000, "term_months": 12, "purpose": "working_capital", "redirect_uri": "https://yourapp.com/return" }'
The body the SDK decodes into a typed Application:
{
"id": "app_8Kd2c9Qm",
"status": "created",
"amount_pence": 2500000,
"term_months": 12,
"handoff_url": "https://apply.credicorp.co.uk/s/3fa7…",
"created_at": "2026-06-29T10:14:02Z"
}From here you read the decision (cc.applications.decision(id), or react to the decision.completed webhook) and, on approval, draw down funds over the Payments API.
Pagination
List endpoints are cursor-paginated. Rather than juggle has_more and the cursor token yourself, every .list() returns a lazy page that you can iterate directly — auto_paging_iter() walks the entire result set, fetching each page only as you reach it:
# Walks every page automatically — one network call per page. pages = cc.applications.list( status="funded", created_after="2026-06-01", limit=50, # page size, max 100 ) for app in pages.auto_paging_iter(): print(app.id, app.amount_pence)
Need raw control — for a "load more" view, or to checkpoint a long backfill? Read one page and pass its cursor back:
page = cc.applications.list(limit=25) page.data # list[Application] page.has_more # bool page.next_cursor # pass back as cursor= next call if page.has_more: nxt = cc.applications.list(limit=25, cursor=page.next_cursor)
Cursors are opaque and stable — safe to persist and resume from later. Don't construct or parse them. See the Pagination reference for the full contract.
Webhooks
Webhooks are how you react to decisions, disbursements and repayments without polling. Every delivery carries a Credicorp-Signature header. Always verify it before trusting the body — an unverified endpoint is a forgery waiting to happen. The SDK's webhooks.construct_event() checks the HMAC, rejects stale timestamps (replay protection) and returns a typed event.
The signature is computed over the raw request body, so hand the verifier the unparsed bytes — use request.get_data(), not request.json:
import os from flask import Flask, request, abort from credicorp import Credicorp, SignatureVerificationError cc = Credicorp() # reads CC_SECRET_KEY app = Flask(__name__) WEBHOOK_SECRET = os.environ["CC_WEBHOOK_SECRET"] @app.post("/webhooks/credicorp") def credicorp_webhook(): try: event = cc.webhooks.construct_event( payload=request.get_data(), # raw bytes sig_header=request.headers["Credicorp-Signature"], secret=WEBHOOK_SECRET, ) except SignatureVerificationError: abort(400, "invalid signature") if event.type == "decision.completed": mark_decision(event.data.application_id, event.data.outcome) elif event.type == "payment.settled": reconcile(event.data.payment_id) return {"received": True}, 200 # 2xx fast, work async
The header itself is a timestamp plus a hex HMAC-SHA256 over {timestamp}.{body}, keyed by your whsec_… secret:
Credicorp-Signature: t=1719660000,v1=4f8b9c1d…e02a
Common event types you'll handle:
| Event | Fires when |
|---|---|
application.updated | Status moves through the lifecycle (created → submitted → in_review). |
decision.completed | The AI decision engine returns an outcome — approved, referred or declined. |
payment.settled | A PISP disbursement or a repayment clears via Faster Payments / Open Banking. |
account.updated | A live facility's balance, arrears state or repayment schedule changes. |
Respond 2xx within 10 seconds. Acknowledge first, then do slow work on a queue (Celery, RQ, …). Anything else is retried with backoff, and deliveries are at-least-once — key your handlers on event.id so a redelivery is a no-op.
Errors
Failed requests raise a typed CredicorpError carrying the HTTP status, the machine-readable code and the request id you'll quote to support. Validation failures itemise the offending fields.
from credicorp import CredicorpError try: cc.applications.create(...) except CredicorpError as err: print(err.status) # 422 print(err.code) # 'validation_error' print(err.request_id) # 'req_1a2b3c' — quote to support print(err.fields) # [{'field': 'amount_pence', …}]
| Status | code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing or expired credentials. The SDK refreshes tokens for you; a persistent 401 means a bad key. |
| 422 | validation_error | The request body failed validation. Inspect err.fields. |
| 429 | rate_limited | Too many requests — retried automatically up to max_retries. |
| 503 | service_unavailable | Transient upstream issue — safe to retry; the SDK already does. |
Going further
- Every method maps 1:1 to the API reference —
cc.payments,cc.identity,cc.accounts,cc.webhooks. - Building an async service? Swap in
AsyncCredicorpandawaitthe same calls;async for app in pages.auto_paging_iter()paginates without blocking. - Run end-to-end against the sandbox with deterministic test companies, then read the Integrate lending guide.
