SDKs

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.

bash
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.

.env
# exported into the process environment
CC_SECRET_KEY=sk_test_9fK2c…
CC_WEBHOOK_SECRET=whsec_4Td1…
python
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

ArgumentTypeDescription
api_keystroptSecret key. Falls back to CC_SECRET_KEY if not passed.
max_retriesintoptRetries on 429 and 5xx with exponential backoff; honours Retry-After. Default 3.
timeoutfloatoptPer-request timeout in seconds. Default 30.0.
api_basestroptOverride 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.

python
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 journey

That is exactly one authenticated HTTP request. The equivalent in raw curl:

bash
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:

201 Created
{
  "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:

python
# 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:

python
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:

python
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:

http
Credicorp-Signature: t=1719660000,v1=4f8b9c1d…e02a

Common event types you'll handle:

EventFires when
application.updatedStatus moves through the lifecycle (created → submitted → in_review).
decision.completedThe AI decision engine returns an outcome — approved, referred or declined.
payment.settledA PISP disbursement or a repayment clears via Faster Payments / Open Banking.
account.updatedA 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.

python
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', …}]
StatuscodeMeaning
401unauthorizedMissing or expired credentials. The SDK refreshes tokens for you; a persistent 401 means a bad key.
422validation_errorThe request body failed validation. Inspect err.fields.
429rate_limitedToo many requests — retried automatically up to max_retries.
503service_unavailableTransient upstream issue — safe to retry; the SDK already does.

Going further

  • Every method maps 1:1 to the API referencecc.payments, cc.identity, cc.accounts, cc.webhooks.
  • Building an async service? Swap in AsyncCredicorp and await the 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.