Node.js SDK
The official Credicorp client for Node.js and TypeScript. 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. Node.js 18 LTS or newer (native fetch and crypto.webcrypto). Ships first-class TypeScript types — no @types package needed. Both ESM and CommonJS builds are included.
Install
Add the package from npm. The SDK has zero runtime dependencies — everything is built on the platform standard library, so it stays small in your container image.
npm install @credicorp/sdk # or: pnpm add @credicorp/sdk · yarn add @credicorp/sdk
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.
# .env — keep out of version control
CC_SECRET_KEY=sk_test_9fK2c…
CC_WEBHOOK_SECRET=whsec_4Td1…import { Credicorp } from '@credicorp/sdk'; const cc = new Credicorp(process.env.CC_SECRET_KEY!, { // all optional — sensible defaults shown maxRetries: 3, // retries 429 / 5xx with backoff timeoutMs: 30_000, });
The constructor also accepts { apiBase } to pin a host, and an OAuth clientId/clientSecret pair instead of a secret key when you call the partner plane on behalf of multiple merchants. Tokens are minted lazily, cached in memory and refreshed before they expire — you never touch /oauth/token directly.
Constructor options
| Option | Type | Description | |
|---|---|---|---|
maxRetries | number | opt | Retries on 429 and 5xx using exponential backoff with jitter; honours Retry-After. Default 3. |
timeoutMs | number | opt | Per-request timeout in milliseconds. Default 30000. |
apiBase | string | opt | Override the API host. Defaults to https://api.credicorp.co.uk. |
fetch | typeof fetch | opt | Inject a custom fetch (instrumented or proxied) for tracing and tests. |
Your first call — create an application
This creates a business-loan application for a UK incorporated company and returns a handoffUrl you redirect the applicant to. Amounts are always in pence to avoid floating-point rounding. The SDK attaches an Idempotency-Key automatically when you don't supply one, but passing your own (an order id, say) makes retries provably safe.
const application = await cc.applications.create({ business: { companyNumber: '16093826' }, amountPence: 2_500_000, // £25,000 termMonths: 12, purpose: 'working_capital', redirectUri: 'https://yourapp.com/return', }, { idempotencyKey: 'order_88421' }); console.log(application.id); // 'app_8Kd2c9Qm' console.log(application.handoffUrl); // hosted apply journey
That 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 (poll cc.applications.decision(id) or, better, 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, the SDK exposes an async iterator that fetches each page lazily as you consume it. Walk an entire result set with a plain for await … of loop:
// Walks every page automatically — one network call per page. for await (const app of cc.applications.list({ status: 'funded', createdAfter: '2026-06-01', limit: 50, // page size, max 100 })) { console.log(app.id, app.amountPence); }
Need raw control — for a "load more" button, or to checkpoint a long backfill? Request a single page and read the cursor yourself:
const page = await cc.applications.list({ limit: 25 }).page(); page.data; // Application[] page.hasMore; // boolean page.nextCursor; // pass back as { cursor } next call if (page.hasMore) { const next = await cc.applications .list({ limit: 25, cursor: page.nextCursor }).page(); }
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.constructEvent() 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 — mount express.raw() on the webhook route, not express.json():
import express from 'express'; import { Credicorp } from '@credicorp/sdk'; const cc = new Credicorp(process.env.CC_SECRET_KEY!); const app = express(); app.post('/webhooks/credicorp', express.raw({ type: 'application/json' }), // raw body required (req, res) => { let event; try { event = cc.webhooks.constructEvent( req.body, // Buffer req.header('Credicorp-Signature')!, process.env.CC_WEBHOOK_SECRET!, ); } catch { return res.status(400).send('invalid signature'); } switch (event.type) { case 'decision.completed': markDecision(event.data.applicationId, event.data.outcome); break; case 'payment.settled': reconcile(event.data.paymentId); break; } res.json({ received: true }); // 2xx fast, then process async });
The header itself looks like the line below — a timestamp and 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. 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 throw 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.
import { CredicorpError } from '@credicorp/sdk'; try { await cc.applications.create({ /* … */ }); } catch (err) { if (err instanceof CredicorpError) { console.error(err.status); // 422 console.error(err.code); // 'validation_error' console.error(err.requestId); // 'req_1a2b3c' — quote to support console.error(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 maxRetries. |
| 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. - Run end-to-end against the sandbox with deterministic test companies before requesting live access.
- New to the model? Start with Core concepts, then the Integrate lending guide.
