{"site":"Credicorp Developer Portal","count":493,"docs":[{"t":"AI crawlers and GEO on the developer portal","u":"/public-api/ai-crawlers-and-geo/","c":"Public API guides","e":"Public API","s":"The developer portal is built to be read by machines, not just people. It welcomes AI crawlers via robots.txt, llms.txt and ai.txt, ships structured data on every page, and offers the MCP server so an agent can query Credicorp facts live. This is Generative Engine Optimisation (GEO): making sure AI answers about Credicorp are accurate because they come from the source.","b":"Crawl policy The portal’s robots.txt explicitly allows GPTBot, ClaudeBot, PerplexityBot and Google-Extended, and it ships an llms.txt summary and an ai.txt usage policy. Crawlers are welcome — the goal is accurate coverage, not exclusion. Structured data Every content page carries a JSON-LD graph — Organization, WebSite, BreadcrumbList and Article or FAQPage — so a crawler can parse the meaning, not just the text. Breadcrumbs are threaded to the apex so the estate reads as one site. Live grounding via MCP Beyond static content, the MCP server lets an agent query products, quotes and eligibilit"},{"t":"API key","u":"/glossary/api-key/","c":"Developer glossary","e":"Glossary","s":"API key — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is An API key is a static token a caller sends to identify itself to an API. Keys are simple but must be kept secret, because anyone holding one can act as the caller. At Credicorp The public ring deliberately uses no API key — there is nothing per-caller to gate, so a key would add a secret to leak while protecting nothing. Keys and OAuth2 tokens are features of the authenticated partner surface, where callers must be identified. See partner API keys. Where do I get a public-ring API key? You do not — the public ring needs none. Keys exist only for the authenticated partner API."},{"t":"API ring","u":"/glossary/ring/","c":"Developer glossary","e":"Glossary","s":"A ring is an authentication boundary in the Credicorp API. The public ring is unauthenticated; the partner ring is OAuth-gated. The ring sets the auth, the limits and the data class.","b":"Definition Credicorp groups endpoints into rings by trust level. The public ring (/public/v1) is unauthenticated and returns published figures; the partner ring (/partner/v1) is token-gated and handles customer, decision and payment data. There are further internal rings the public docs do not cover. In plain terms A tier of the API grouped by how much trust it needs. Why it matters here Knowing which ring an endpoint is in tells you the auth, the rate limit and whether it touches sensitive data. See public vs partner."},{"t":"Access token","u":"/glossary/access-token/","c":"Developer glossary","e":"Glossary","s":"An access token is the short-lived bearer JWT you send as Authorization: Bearer on partner calls. You mint it from your client credentials and re-mint it when it expires.","b":"Definition An access token is a signed, time-limited credential — a JWT — that proves your right to call the partner ring. It carries the granted scopes and an expiry, and is verifiable against the JWKS. In plain terms The temporary pass your server shows on every authenticated call. Why it matters here Cache it and reuse it until just before expiry rather than minting per request. See the token lifecycle."},{"t":"Add a quote illustration to your site","u":"/recipes/add-a-quote-illustration-to-your-site/","c":"Integration recipes","e":"Recipe","s":"Show visitors an example cost from Credicorp’s own data. Call the MCP GetQuote tool with an amount and term, render the indicative figure, and label it plainly as an illustration. Link out to the repayment calculator and the application so visitors can get their own real numbers.","b":"Step 1 — call GetQuote { \"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{ \"name\":\"GetQuote\",\"arguments\":{ \"amount\":25000,\"term_months\":12 } } } Step 2 — render and label Show the returned figure with a clear \"indicative example — not an offer\" label. Never present it as a firm quote. See indicative quote. Step 3 — link to real numbers Point visitors to the repayment calculator for their own figures and the application to proceed. Why show an illustration at all A concrete number does something a page of prose cannot: it turns an abstract product into something a business owner can pictu"},{"t":"At-least-once delivery","u":"/glossary/at-least-once/","c":"Developer glossary","e":"Glossary","s":"At-least-once delivery guarantees every event arrives one or more times — never zero, but sometimes twice. Consumers must de-duplicate on the event ID.","b":"Definition At-least-once is the delivery guarantee for Credicorp webhooks: retries mean an event may be delivered more than once and occasionally out of order, but it will not be silently dropped. The trade is that consumers must handle duplicates. In plain terms Every event definitely arrives, but you might get it more than once. Why it matters here De-duplicate on the event ID and order by state, not arrival. See building an idempotent consumer."},{"t":"At-least-once delivery","u":"/glossary/at-least-once-delivery/","c":"Developer glossary","e":"Glossary","s":"At-least-once delivery guarantees every event arrives at least one time — and possibly more than once — so the receiver must be built to tolerate duplicates.","b":"Definition At-least-once is the delivery guarantee Credicorp webhooks use. It means no event is ever silently dropped, but a single event may be delivered more than once (after a slow acknowledgement or a retry). The trade-off for never losing an event is that your handler must be idempotent. Contrast with at-most-once (may drop, never duplicates) and exactly-once (neither, but far harder to guarantee). Why not exactly-once? Exactly-once across a network boundary is extremely hard and usually an illusion. At-least-once plus an idempotent consumer gives you the same practical result more simply"},{"t":"Authentication on the public ring (there is none)","u":"/public-api/authentication-on-the-public-ring/","c":"Public API guides","e":"Public API","s":"The public ring has no authentication — and that is the correct design. There is nothing to authenticate: every endpoint either returns public information or accepts a safe, validated submission. Instead of a credential, safety comes from rate limiting, strict input validation, server-fixed response shapes and a hard rule that no per-customer PII ever crosses this ring.","b":"Why no credential Authentication exists to prove who is calling so the server can decide what they may see. On the public ring there is no per-caller data to gate: the loyalty ladder, product list and published pages are the same for everyone, and an enquiry or consent snapshot is safe to accept from anyone. A credential would add friction and a secret to leak while protecting nothing — so the ring has none. What replaces it Three mechanisms do the job a credential would otherwise do:Rate limiting — 60 requests per 60 seconds per IP stops one caller monopolising the ring.Validation — every inp"},{"t":"Authoritative decisioning","u":"/glossary/authoritative-decisioning/","c":"Developer glossary","e":"Glossary","s":"Authoritative decisioning means the API returns the real credit decision, not an advisory suggestion you re-evaluate. The decision is final; the one manual gate downstream is money-out.","b":"Definition Credicorp's decisioning engine produces the actual outcome of an application, and the decision endpoint returns exactly that. There is no separate 'advisory' step for a human to confirm the decision itself — the model is authoritative. The single governed manual control in the whole flow is money-out. In plain terms The decision the API gives you is the real answer, not a hint. Why it matters here It keeps integrations fast — instant, real decisions — while the one irreversible step (funds leaving) stays governed. See money-out: the one gate."},{"t":"Base URL","u":"/glossary/base-url/","c":"Developer glossary","e":"Glossary","s":"The base URL is the root every endpoint hangs off: https://hub.credicorp.co.uk, plus the ring prefix /public/v1 or /partner/v1.","b":"Definition Credicorp's public and partner handlers are served from the hub host, https://hub.credicorp.co.uk. Append the ring prefix and the endpoint path — for example /public/v1/products. There is no separate host for the public ring; sandbox versus live differs by project and credentials, not by base host. In plain terms The web address everything is built on top of. Why it matters here Hard-code the ring prefix (pin /v1) but read figures from the API rather than assuming them. See versioning."},{"t":"Bearer token","u":"/glossary/bearer-token/","c":"Developer glossary","e":"Glossary","s":"A bearer token grants access to whoever presents it — no additional proof of identity. That is why Credicorp tokens are short-lived and sent only over TLS.","b":"Definition 'Bearer' means possession is authorisation: anyone holding the token can use it. Credicorp access tokens are bearer tokens, sent as Authorization: Bearer &lt;token&gt;. Their short lifetime limits the window a leaked token is useful. In plain terms A token that works for whoever has it, like cash — so guard it. Why it matters here Never log or expose a bearer token; keep it server-side and re-mint on expiry. See the token lifecycle."},{"t":"Build a product-comparison page with MCP","u":"/recipes/build-a-product-comparison-page/","c":"Integration recipes","e":"Recipe","s":"Power a product-comparison page from Credicorp’s own data via MCP. Call ListProducts for the line-up (Business Loan, Credicorp Flex, Credicorp Slice), ProductDetails for each product’s specifics, and GetQuote for an indicative figure — all read-only, all from the source, so your comparison never drifts from the truth.","b":"Step 1 — list the products { \"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{ \"name\":\"ListProducts\",\"arguments\":{} } } Step 2 — pull details for each Call ProductDetails per product to get its specifics. Render them side by side. Because you read from the MCP server, a change in our product data flows straight into your comparison without a copy-paste edit. Step 3 — add an indicative quote Use GetQuote with an amount and term to show an example figure. Label it clearly as indicative — it is not an offer. Keep it honest The tools are read-only and cannot apply or transact. Send users who"},{"t":"Build a resilient API client","u":"/recipes/build-a-resilient-api-client/","c":"Integration recipes","e":"Recipe","s":"A production client needs five habits. Set timeouts, retry only transient errors with jittered backoff, honour Retry-After, attach idempotency keys to writes, and branch on error.code. Put together, they turn flaky networks and rate limits into non-events.","b":"The checklist Every robust client does these five things:Set a request timeout so a hung call fails fast.Retry only 429 and 5xx, with jittered backoff.Honour Retry-After on a 429.Attach an idempotency key to every write.Branch on error.code, not the HTTP status alone. Wire it together A thin wrapper composes them:async function call(method, path, body, key) { return withRetry(() => fetch(BASE + path, { method, signal: AbortSignal.timeout(8000), headers: { 'Content-Type':'application/json', ...(key ? {'Idempotency-Key': key} : {}) }, body: body && JSON.stringify(body), })); } Branch on the outc"},{"t":"Build a status badge from a cacheable read","u":"/recipes/build-a-status-badge-from-a-read/","c":"Integration recipes","e":"Recipe","s":"Show a \"systems normal\" badge driven by a cacheable public read. Poll a cheap read like the loyalty tiers on a slow interval, cache the result, and flip the badge on a 200 with a fresh timestamp. It is a light-touch liveness signal that never trips the rate limit.","b":"Poll a cheap read Call the loyalty endpoint from your server every minute or two — far under the 60/60 s limit — and cache the outcome for all page views to share. Define the badge states Green on a 200 with a recent generated timestamp; amber on a 429 (that is your probe, not the API — slow down); red on a 5xx or timeout. Never probe writes Drive the badge from a read only. Posting enquiries to check health pollutes real data and burns rate budget. See monitoring. Why a read makes a good probe A status badge is only trustworthy if the check behind it is cheap, honest and side-effect free. A c"},{"t":"Build an application assistant with MCP","u":"/recipes/build-an-application-assistant-with-mcp/","c":"Integration recipes","e":"Recipe","s":"Compose the public MCP tools into an assistant: eligibility_criteria to qualify, get_quote to price, how_to_apply to guide, then hand the user to the hosted application — every figure live and reconciled.","b":"The flow A good lending assistant follows the same order a human adviser would. Register the public MCP server as a tool source, then let the model call, in sequence: eligibility_criteria to confirm the company can apply, get_quote to show a real figure, how_to_apply to explain the steps, and list_products if the user is choosing between products. Why MCP beats scraping Because the tools read the hub's authoritative product and pricing config, the assistant quotes the real numbers rather than stale figures scraped from a page — the same numbers the marketing site shows. That is the whole point"},{"t":"Build an idempotent webhook consumer","u":"/recipes/build-an-idempotent-webhook-consumer/","c":"Integration recipes","e":"Recipe","s":"Credicorp delivers webhooks at-least-once, so duplicates and out-of-order arrivals are normal. De-duplicate on the event ID, trust the event's state over arrival order, and make a replay a no-op.","b":"De-duplicate on event ID Record every event ID you have processed. On receipt, check the store first: if you have seen the ID, acknowledge with a 2xx and stop — the work is already done. This turns at-least-once delivery into effectively exactly-once processing on your side. Order by state, not arrival Retries and parallel delivery mean a 'settled' event can arrive before a 'created' one. Do not assume arrival order. Key your state machine on the event's own type and timestamp, and ignore a transition that would move a resource backwards. Verify the signature first — see verifying a signature."},{"t":"Business Loan (product)","u":"/glossary/business-loan-product/","c":"Developer glossary","e":"Glossary","s":"The Business Loan is Credicorp's short-term loan for a one-off, small sum on a fixed term with a simple repayment schedule. It is the only product the public quote endpoint prices.","b":"Definition The Business Loan is a short-term business loan for a one-off, small sum, repaid on a simple fixed schedule. Because it has a pure amount-and-term shape, it is the only product quotable via quote/onetime and get_quote, bounded to £50–£500 / 14–84 days. In plain terms Credicorp's simple, fixed-term small-sum loan. Why it matters here Lending is to the UK limited company, with no personal guarantee. Contrast Flex and Slice. See the products endpoint."},{"t":"CORS","u":"/glossary/cors/","c":"Developer glossary","e":"Glossary","s":"CORS — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is CORS (cross-origin resource sharing) is the browser mechanism that decides whether JavaScript on one origin may read a response from another. The server signals permitted origins with response headers. In the Credicorp API If you call a public endpoint directly from browser JavaScript on your own domain, CORS governs whether the browser lets you read the response. For submissions like consent, the intended pattern is a trusted server-side forward from your edge, which sidesteps CORS entirely and keeps your edge as the trust boundary. Should I call the consent endpoint from browser J"},{"t":"CSRF","u":"/glossary/csrf/","c":"Developer glossary","e":"Glossary","s":"CSRF — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is CSRF (cross-site request forgery) tricks a logged-in user’s browser into making an unwanted request. The usual defence is a per-session token the server checks. In the Credicorp API The public ring is unauthenticated, so classic session-CSRF does not apply the same way. The consent endpoint accepts a forwarded snapshot without a CSRF check precisely because the edge already validated the visitor’s own form — the edge is the origin/CSRF trust boundary. Do not call it straight from the browser. Why is there no CSRF token on the consent endpoint? Because it expects a trusted server-sid"},{"t":"Cache OAuth tokens across your workers","u":"/recipes/cache-oauth-tokens/","c":"Integration recipes","e":"Recipe","s":"Mint an access token once and share it across every worker. Key the cache by (client, scope), store the absolute expiry, and re-mint just before it lapses — never per request.","b":"Why cache at all The token endpoint is itself rate-limited, and minting a token per request is pure waste — the same token works for every call until it expires. A single live token, shared across processes, is both faster and cheaper. See the token lifecycle. Key and store correctly Key the cache by (client_id, scope-set) so a narrowly-scoped token and a broadly-scoped one do not overwrite each other. Store the absolute expiry — now + expires_in − margin — where the margin (a few seconds) covers clock skew and in-flight requests. In a multi-process deployment, put the token in a shared cache "},{"t":"Cache TTL","u":"/glossary/cache-ttl/","c":"Developer glossary","e":"Glossary","s":"Cache TTL — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is TTL (time-to-live) is how long a cached value is served before it is treated as stale and refreshed. A longer TTL cuts load but risks serving older data; a shorter one is fresher but busier. For the public API Because the loyalty ladder and biller registry change rarely and carry no PII, a TTL of around five minutes is a good balance — almost all requests hit cache, and a few-minute lag is harmless. Use the response’s generated timestamp as your freshness marker. Widget assets are static and can have a much longer TTL. See Caching. What TTL should I use for the loyalty tiers? Around"},{"t":"Cache the loyalty ladder for performance","u":"/recipes/cache-the-loyalty-ladder/","c":"Integration recipes","e":"Recipe","s":"Cache the loyalty ladder and you all but remove it from your load budget. A five-minute shared cache, keyed on the response’s generated timestamp, keeps the ladder fresh while serving nearly every request from cache. Because the response carries no PII, a public shared cache is perfectly safe.","b":"Pick a TTL The tier config changes rarely, so a five-minute TTL is a good balance — a stale ladder for a few minutes is harmless, and you cut the origin calls dramatically. Use the freshness marker Each response carries a generated UTC timestamp. Use it as the freshness dimension of your cache key so you can tell a fresh value from a stale one and invalidate cleanly. Stale-while-revalidate If your cache supports it, serve the stale copy immediately while refreshing in the background. The user never waits on the origin, and the ladder is at most one TTL behind. Public vs private cache There is "},{"t":"Caching public-API responses","u":"/public-api/caching-public-api-responses/","c":"Public API guides","e":"Public API","s":"Most public-API reads are safe to cache, and caching them is the single biggest performance win. The loyalty ladder, CMS pages, biller registry and widget assets all change infrequently and carry no PII, so a short shared cache removes almost all load while keeping content fresh. The generated timestamp in each response is your freshness marker.","b":"What is safe to cache Every read on the public ring is cacheable because none of it is per-user. The rule of thumb:Loyalty tiers and Slice billers — cache for around five minutes; the config rarely changes and each response carries a generated UTC timestamp you can use as the cache key’s freshness dimension.CMS pages — cache keyed on the page key and its updated field; invalidate when updated moves.Widget script and stylesheet — static assets; cache aggressively at the CDN and browser. What not to cache Do not cache POST responses — enquiries, consent and support-chat replies are one-shot writ"},{"t":"Call the MCP server with JSON-RPC","u":"/recipes/call-the-mcp-server-with-json-rpc/","c":"Integration recipes","e":"Recipe","s":"Drive the public MCP server by hand with three JSON-RPC calls: initialize, tools/list, then tools/call on get_quote. No token, no SDK — just POSTs to /public/v1/mcp.","b":"1. Initialize curl -s -X POST https://hub.credicorp.co.uk/public/v1/mcp \\ -H 'Content-Type: application/json' \\ -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}'The result advertises the server's capabilities — see the endpoint reference. 2. List the tools curl -s -X POST https://hub.credicorp.co.uk/public/v1/mcp \\ -H 'Content-Type: application/json' \\ -d '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\",\"params\":{}}'You get the six public tools with their input schemas — the catalogue. 3. Call get_quote curl -s -X POST https://hub.credicorp.co.uk/public/v1/mcp \\ -H 'Content-Type: a"},{"t":"Call the public API from Node.js","u":"/recipes/call-the-public-api-from-node/","c":"Integration recipes","e":"Recipe","s":"Call the Credicorp public API from Node with the built-in fetch. Read the loyalty tiers, post an enquiry, and handle a 429 — all with no dependencies and no key. This recipe gives you copy-paste snippets for both a read and a write.","b":"Read the loyalty tiers const res = await fetch('https://hub.credicorp.co.uk/public/v1/loyalty/tiers'); if (res.status === 429) { const wait = Number(res.headers.get('retry-after') || 1); await new Promise(r =&gt; setTimeout(r, wait * 1000)); } const { tiers } = await res.json(); console.log(tiers.map(t =&gt; `${t.name}: ${t.benefit.arrangement_fee_discount_pct}%`)); Submit an enquiry const res = await fetch('https://hub.credicorp.co.uk/public/v1/enquiries', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ form: 'contact-us', dept: 'support', fields: { n"},{"t":"Call the public API from PHP","u":"/recipes/call-the-public-api-from-php/","c":"Integration recipes","e":"Recipe","s":"Call the Credicorp public API from PHP with curl. Read the loyalty ladder and post an enquiry using the curl extension, with a check for the 429 rate-limit response. Copy-paste snippets for both a read and a write, no key needed.","b":"Read the loyalty tiers $ch = curl_init('https://hub.credicorp.co.uk/public/v1/loyalty/tiers'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code === 200) { $data = json_decode($body, true); foreach ($data['tiers'] as $t) { echo $t['name'].': '.$t['benefit']['arrangement_fee_discount_pct'].\"%\\n\"; } } Submit an enquiry $payload = json_encode([ 'form' =&gt; 'contact-us', 'dept' =&gt; 'support', 'fields' =&gt; ['name' =&gt; 'Jordan', 'email' =&gt; 'jordan@example-ltd.co.uk', 'consent' =&gt; 'yes'], ]); "},{"t":"Call the public API from Python","u":"/recipes/call-the-public-api-from-python/","c":"Integration recipes","e":"Recipe","s":"Call the Credicorp public API from Python with requests. Read the loyalty ladder and post an enquiry, honouring the Retry-After header on a 429. Two short snippets, no key, ready to paste into a script or service.","b":"Read the loyalty tiers import requests, time r = requests.get('https://hub.credicorp.co.uk/public/v1/loyalty/tiers') if r.status_code == 429: time.sleep(int(r.headers.get('Retry-After', '1'))) r = requests.get('https://hub.credicorp.co.uk/public/v1/loyalty/tiers') for t in r.json()['tiers']: print(t['name'], t['benefit']['arrangement_fee_discount_pct'], '%') Submit an enquiry import requests r = requests.post( 'https://hub.credicorp.co.uk/public/v1/enquiries', json={ 'form': 'contact-us', 'dept': 'support', 'fields': {'name': 'Jordan', 'email': 'jordan@example-ltd.co.uk', 'consent': 'yes'}, },"},{"t":"Capture a lead with POST /public/v1/enquiries","u":"/recipes/capture-a-lead-via-enquiries/","c":"Integration recipes","e":"Recipe","s":"Send accepted form submissions to the public enquiries endpoint. Capture consent first, run your own anti-abuse client-side, and treat the response as a fail-open acknowledgement.","b":"Capture consent, then submit Record the visitor's cookie consent (via POST /public/v1/consent) and the enquiry's own consent, then POST the enquiry:await fetch('https://hub.credicorp.co.uk/public/v1/enquiries', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, email, message, consent: true }) });See the endpoint reference for the field shape. Run anti-abuse before submitting The endpoint is unauthenticated, so put the heavyweight defences on your side before the call: a honeypot field, a time-trap (reject sub-second submissions), and a per-IP cap. "},{"t":"Choosing an SDK or raw HTTP","u":"/public-api/choosing-an-sdk-or-raw-http/","c":"Public API guides","e":"Getting started","s":"Use an SDK when you want auth, retries, pagination and typing handled for you. Use raw HTTP for a language with no SDK, a thin serverless function, or when you need total control.","b":"What an SDK gives you A Credicorp SDK wraps the same REST surface but handles the tedious parts: minting and caching OAuth tokens, retrying with back-off on 429/5xx, threading cursors, and giving you typed request and response objects. For most integrations it is the faster, safer path — you write business logic, not plumbing. When raw HTTP wins Reach for plain HTTP when there is no SDK for your language, when you are in a constrained runtime (a tiny serverless function, an edge worker) where a dependency is costly, or when you need behaviour the SDK does not expose. The API is deliberately si"},{"t":"Choosing between webhooks and polling","u":"/public-api/choosing-webhooks-or-polling/","c":"Public API guides","e":"Public API","s":"Reach for webhooks when you need to react to events promptly, and poll only when you cannot host an endpoint. Webhooks push each event within seconds and save your rate-limit budget; polling is simpler but wastes requests and adds latency. The most robust integrations use both.","b":"The trade-off WebhooksPollingLatencySecondsUp to your poll intervalRequest costNone (push)Every poll counts against the rate limitNeeds an endpointYes (public HTTPS)NoHandles missed eventsRetries + replayNaturally re-reads state Use both React to webhooks in real time, and run an occasional reconciliation poll to catch anything missed during an outage past the 72-hour retry window. See Replay and recover missed webhooks. I cannot host an endpoint — what do I do? Poll the relevant read endpoint on a sensible interval and cache aggressively to stay under the rate limit. See [Recover from rate li"},{"t":"Choosing the right endpoint","u":"/public-api/choosing-the-right-endpoint/","c":"Public API guides","e":"Public API","s":"Not sure which public endpoint you need? Start here. This guide maps common goals to the right endpoint — reading products, showing the loyalty ladder, taking a contact enquiry, recording cookie consent, embedding support chat, or connecting an AI agent — so you pick the shortest path to what you want.","b":"I want to show product info For a human page, read the product pages or use the CMS endpoint for published copy. For an AI agent, use the ListProducts and ProductDetails MCP tools. I want to show the loyalty ladder Read GET /public/v1/loyalty/tiers and cache it. See render the ladder. I want to take a contact enquiry Post to POST /public/v1/enquiries with a consent flag. See submit a contact form. I want cookie consent recorded Forward from your edge to POST /public/v1/consent. See record cookie consent. I want live chat Embed the support widget. See embed the widget. I want an AI agent to ans"},{"t":"Client-credentials grant","u":"/glossary/client-credentials/","c":"Developer glossary","e":"Glossary","s":"The client-credentials grant is the OAuth 2.0 machine-to-machine flow: a server exchanges a client ID and secret for a bearer token, with no user and no redirect.","b":"Definition Client credentials is the OAuth 2.0 grant for server-to-server auth. There is no end user, so no browser redirect and no authorisation code — your server posts its client_id and client_secret to the token endpoint and gets back a bearer access token. In plain terms How one server proves it is allowed to call another, without a person logging in. Why it matters here It is how the partner ring authenticates. See client credentials in depth and the token endpoint."},{"t":"Common integration patterns","u":"/public-api/integration-patterns/","c":"Public API guides","e":"Patterns","s":"Most integrations fall into five shapes: a quote widget, an embedded application, a comparison listing, an AI assistant and a back-office sync. Each maps cleanly to the public or partner ring.","b":"Public-ring patterns A quote widget renders a live figure from the quote endpoint; a comparison listing reads products and pricing; an AI assistant chains the MCP tools. None needs a token — they read published figures and hand qualified users to apply. Partner-ring patterns An embedded application takes the application in your own UI and submits it to POST /applications, reads the decision and provisions a payment; a back-office sync mirrors application and payment state into your systems via webhooks. Both need OAuth and a project. Picking your pattern Start from the outcome: showing figures"},{"t":"Connect an AI agent to Credicorp over MCP","u":"/recipes/connect-an-ai-agent-via-mcp/","c":"Integration recipes","e":"Recipe","s":"Give an AI agent accurate Credicorp product knowledge by connecting it to the MCP server. Point any Model Context Protocol client at /public/v1/mcp, let it initialize and tools/list, and it can call six read-only tools to answer questions about products, quotes, eligibility, how to apply and loyalty tiers — all from the source.","b":"Step 1 — inspect the server card A plain GET returns the server card: protocol revision, server identity (Credicorp v1.0.0) and a summary of each tool.curl -sS https://hub.credicorp.co.uk/public/v1/mcp Step 2 — point your client at it Configure your MCP client with the endpoint URL. It will initialize, negotiate the 2025-06-18 revision, and fetch the tool schemas via tools/list. Step 3 — call a tool { \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\", \"params\": { \"name\": \"ListProducts\", \"arguments\": {} } } What the agent can and cannot do The tools are read-only: the agent can describe product"},{"t":"Constant-time comparison","u":"/glossary/constant-time-comparison/","c":"Developer glossary","e":"Glossary","s":"A constant-time comparison checks two values without returning early on the first mismatch, so an attacker cannot learn a secret from how long the check took.","b":"Definition A naive string compare stops at the first differing byte, so its running time leaks how many leading bytes matched. A constant-time comparison always examines the full length, removing that timing side channel — essential when comparing an HMAC signature. In plain terms Comparing secrets in a way that does not leak them through timing. Why it matters here Use your language's constant-time helper (for example hmac.compare_digest) when verifying webhooks. See verifying a signature."},{"t":"Content-Type","u":"/glossary/content-type/","c":"Developer glossary","e":"Glossary","s":"Content-Type — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is The Content-Type header tells the server what format a request body is in. For JSON payloads it is application/json. In the Credicorp API When you POST to a Credicorp public endpoint, set Content-Type: application/json so the body parses correctly. The API returns JSON too — you can send Accept: application/json to make that explicit, though it is the default. What happens if I forget the Content-Type? The server may fail to parse your body, returning a 400. Always send `Content-Type: application/json` when posting JSON."},{"t":"Correlation / request ID","u":"/glossary/correlation-id/","c":"Developer glossary","e":"Glossary","s":"A correlation ID (or request ID) is the identifier the API returns on a response so you can trace one call through your logs and Credicorp's, and quote it to support.","b":"Definition Each response carries an identifier that uniquely tags the request. Log it alongside status and latency so you can trace a specific call, tie an outbound request to its later webhook, and give support an exact reference when something needs investigating. In plain terms A tracking number for each API call. Why it matters here It is the single most useful thing to capture for observability."},{"t":"Credicorp Flex (product)","u":"/glossary/flex-product/","c":"Developer glossary","e":"Glossary","s":"Credicorp Flex is a revolving business credit facility: draw what you need up to a limit and repay flexibly, with cost accruing on the balance you hold for the days you hold it.","b":"Definition Flex is a revolving facility rather than a one-off loan. You draw against a limit as needed and repay flexibly; interest accrues on the outstanding balance for the days it is held, so cost tracks usage. It is previewed via quote/flex and described in product_details. In plain terms A revolving line of business credit you draw on and repay as you like. Why it matters here Because it is revolving, its cost is representative, not a fixed total. Contrast the Business Loan. See products."},{"t":"Credicorp Slice","u":"/glossary/credicorp-slice/","c":"Developer glossary","e":"Glossary","s":"Credicorp Slice — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is Credicorp Slice is the Credicorp product that lets a UK business spread a large supplier invoice or tax bill across several months instead of paying it in one go. It is one of the three products the MCP server can describe. In the public API Slice surfaces on the public ring through the accepted-biller registry endpoint, which lists the billers a customer can spread against, in a public-safe projection (name, category, min, max). The registry is feature-flag gated. For the product itself, see the Slice product page. Where do I list Slice billers? Via `GET /public/v1/slice/billers`, "},{"t":"Credicorp Slice (product)","u":"/glossary/slice-product/","c":"Developer glossary","e":"Glossary","s":"Credicorp Slice lets a company pay an accepted supplier or biller now and repay Credicorp over a short schedule. Its accepted-biller list is published and feature-flagged.","b":"Definition Slice is a pay-a-biller product: Credicorp settles an accepted supplier or biller for you, and you repay Credicorp on a short schedule. The list of accepted billers is published at slice/billers and is feature-flagged, so it may be empty when the surface is off. In plain terms Pay a supplier now, repay Credicorp shortly after. Why it matters here Handle an empty biller list as 'none currently available'. Contrast Business Loan and Flex. See products."},{"t":"Cross-origin request","u":"/glossary/cross-origin/","c":"Developer glossary","e":"Glossary","s":"Cross-origin request — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is A cross-origin request is one where the calling page’s origin — its scheme, host and port — differs from the API’s. Browsers restrict what JavaScript can do with cross-origin responses via CORS. In the Credicorp API Server-to-server calls are not subject to CORS, which is one reason the Credicorp public write endpoints are designed to be called from a trusted server-side client rather than the browser. Reads are simpler, but heavy browser use should still go through your own cache/proxy. Are server-to-server calls affected by CORS? No. CORS is a browser mechanism. A server-side call"},{"t":"Cross-site scripting (XSS)","u":"/glossary/xss/","c":"Developer glossary","e":"Glossary","s":"Cross-site scripting (XSS) — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is Cross-site scripting (XSS) is an attack where malicious script is injected into a page and runs in a victim’s browser. Untrusted HTML is the classic vector. In the Credicorp API To reduce XSS risk, the CMS endpoint sanitises its HTML before serving it. Because you are ultimately responsible for what renders on your page, still apply your framework’s escaping and a Content-Security-Policy. See HTML sanitisation. Does sanitisation alone stop XSS? It removes the obvious vectors, but you should layer your own escaping and a Content-Security-Policy at render time. Defence in depth is the"},{"t":"Cursor pagination","u":"/glossary/cursor-pagination/","c":"Developer glossary","e":"Glossary","s":"Cursor pagination pages a collection with an opaque token instead of a page number, so it stays stable when rows are inserted mid-iteration. Follow next_cursor until it is null.","b":"Definition Each page response carries an opaque next_cursor; you pass it back to fetch the next page and stop when it is null. Because the cursor encodes a position rather than an offset, inserting rows during iteration never causes you to skip or repeat an item. In plain terms Paging with a bookmark token rather than page numbers, so nothing shifts under you. Why it matters here Used by every partner list endpoint. See paginating a collection."},{"t":"Data and privacy on the public ring","u":"/public-api/data-and-privacy-on-the-public-ring/","c":"Public API guides","e":"Public API","s":"The public ring returns published figures only — no customer data, ever. The two write endpoints record real rows (a lead, a consent choice) and require consent; everything else is safe, cacheable config.","b":"Reads carry no customer data Every public read — products, pricing, quotes, loyalty, MCP — returns only published, non-sensitive figures. There is no customer record on this ring, which is exactly why it needs no authentication and is safe to cache. The two writes are special Enquiries and consent do record real data — a lead and a PECR consent snapshot. Both require the visitor's consent, both are fail-open, and neither returns stored data to the caller. Treat the data you send them with the same care in development as in production. Where customer data actually lives Anything customer-specif"},{"t":"Data you can read vs submit","u":"/public-api/data-you-can-read-vs-submit/","c":"Public API guides","e":"Public API","s":"The public ring is easy to reason about once you split it into reads, submits and off-limits. You can read public vocabulary — products, tiers, published pages, billers — and submit two safe things — enquiries and cookie consent. Everything per-customer or money-moving is off-limits and lives behind authentication.","b":"What you can read The loyalty ladder, published CMS pages, the Slice biller registry, and — via MCP — products, quotes, eligibility and how-to-apply. All public, all keyless, all cacheable. What you can submit Two things: a contact/department enquiry (with mandatory consent) and a cookie-consent snapshot. Both are validated and safe to accept from the open internet. What is off-limits Reading a specific customer’s account, creating or accepting offers, moving money, running a real credit decision — none of these appear on the public ring. They require authentication. See moving to the partner "},{"t":"Dead-letter (expired webhook)","u":"/glossary/dead-letter/","c":"Developer glossary","e":"Glossary","s":"A dead-lettered webhook is one that failed every delivery attempt across the full 72-hour retry window and is then dropped — recover it by re-reading current state from the API.","b":"Definition When a webhook cannot be delivered within its 72-hour retry window — your endpoint was down the whole time — it is dead-lettered: removed from the retry queue rather than retried forever. There is no separate dead-letter feed; the recovery path is to re-read the affected resources from the API and reconcile. Keeping your endpoint healthy inside the window prevents dead-lettering entirely. How do I recover a dead-lettered event? Re-fetch the resource from the read API and reconcile against your records. Your idempotent handler makes re-applying safe. See [Replay and recover missed we"},{"t":"Debug a 422 validation error","u":"/recipes/debug-a-422-validation-error/","c":"Integration recipes","e":"Recipe","s":"A 422 means your request parsed but a value failed a rule. The envelope tells you exactly which: error.code names the rule, error.param names the field. This recipe maps the common codes to their fixes.","b":"Read the envelope Do not guess. The error envelope pinpoints the problem.{ \"error\": { \"type\": \"invalid_request_error\", \"code\": \"consent_required\", \"message\": \"fields.consent must equal \\\"yes\\\".\", \"param\": \"fields.consent\" } } Map code to fix Common enquiry-submission codes:codeFixconsent_requiredSend fields.consent: \"yes\"invalid_emailCorrect fields.emailform_requiredSet the form keypayload_too_largeTrim fields under 16 KiBinvalid_deptUse a valid dept or omit it Do not retry blindly A 422 is deterministic — the same request will fail again. Fix the field, then resend. See Retrying failed reques"},{"t":"Decision reason: adverse_credit","u":"/reference/decision-reason-adverse-credit/","c":"API reference","e":"API reference","s":"adverse_credit is a decision reason value. Adverse credit signals led to a decline. It appears in the <a href=\"/reference/event-decision-referred/\">decision.completed</a> webhook payload under reason. Decline respectfully; do not expose the underlying data in the response.","b":"What it means Adverse credit signals led to a decline. Material adverse markers on the business credit profile. Where you see it It arrives in the webhook payload:{ \"data\": { \"object\": { \"id\": \"dec_5H2N8\", \"enquiry_id\": \"enq_9F3K2P\", \"outcome\": \"declined\", \"reason\": \"adverse_credit\" } } } How to handle it Decline respectfully; do not expose the underlying data in the response.Keep declines respectful and never expose the underlying assessment data. See how to apply and the eligibility criteria. In practice Treat adverse_credit as one branch of your reason handling, not the whole story. For dec"},{"t":"Decision reason: affordability","u":"/reference/decision-reason-affordability/","c":"API reference","e":"API reference","s":"affordability is a decision reason value. The application did not meet affordability criteria. It appears in the <a href=\"/reference/event-decision-referred/\">decision.completed</a> webhook payload under reason. Show a clear, respectful decline and, where relevant, signpost a smaller amount or a later re-apply.","b":"What it means The application did not meet affordability criteria. The assessed cash flow did not support the requested facility. Where you see it It arrives in the webhook payload:{ \"data\": { \"object\": { \"id\": \"dec_5H2N8\", \"enquiry_id\": \"enq_9F3K2P\", \"outcome\": \"declined\", \"reason\": \"affordability\" } } } How to handle it Show a clear, respectful decline and, where relevant, signpost a smaller amount or a later re-apply.Keep declines respectful and never expose the underlying assessment data. See how to apply and the eligibility criteria. In practice Treat affordability as one branch of your r"},{"t":"Decision reason: identity_unverified","u":"/reference/decision-reason-identity-unverified/","c":"API reference","e":"API reference","s":"identity_unverified is a decision reason value. Identity or business verification could not be completed. It appears in the <a href=\"/reference/event-decision-referred/\">decision.completed</a> webhook payload under reason. Ask the customer to re-submit clear documents; the application can proceed once verification passes.","b":"What it means Identity or business verification could not be completed. KYC/KYB checks did not pass or were referred. Where you see it It arrives in the webhook payload:{ \"data\": { \"object\": { \"id\": \"dec_5H2N8\", \"enquiry_id\": \"enq_9F3K2P\", \"outcome\": \"declined\", \"reason\": \"identity_unverified\" } } } How to handle it Ask the customer to re-submit clear documents; the application can proceed once verification passes.Keep declines respectful and never expose the underlying assessment data. See how to apply and the eligibility criteria. In practice Treat identity_unverified as one branch of your r"},{"t":"Decision reason: insufficient_trading_history","u":"/reference/decision-reason-insufficient-trading-history/","c":"API reference","e":"API reference","s":"insufficient_trading_history is a decision reason value. The business had too little trading history to assess. It appears in the <a href=\"/reference/event-decision-referred/\">decision.completed</a> webhook payload under reason. Explain that more trading evidence strengthens a future application.","b":"What it means The business had too little trading history to assess. A newer company without enough visible trading for a confident decision. Where you see it It arrives in the webhook payload:{ \"data\": { \"object\": { \"id\": \"dec_5H2N8\", \"enquiry_id\": \"enq_9F3K2P\", \"outcome\": \"declined\", \"reason\": \"insufficient_trading_history\" } } } How to handle it Explain that more trading evidence strengthens a future application. See how eligibility works.Keep declines respectful and never expose the underlying assessment data. See how to apply and the eligibility criteria. In practice Treat insufficient_tr"},{"t":"Decision reason: out_of_appetite","u":"/reference/decision-reason-out-of-appetite/","c":"API reference","e":"API reference","s":"out_of_appetite is a decision reason value. The request fell outside current lending appetite. It appears in the <a href=\"/reference/event-decision-referred/\">decision.completed</a> webhook payload under reason. Decline clearly; the customer can re-apply if their circumstances change.","b":"What it means The request fell outside current lending appetite. Sector, size or structure outside the current policy window. Where you see it It arrives in the webhook payload:{ \"data\": { \"object\": { \"id\": \"dec_5H2N8\", \"enquiry_id\": \"enq_9F3K2P\", \"outcome\": \"declined\", \"reason\": \"out_of_appetite\" } } } How to handle it Decline clearly; the customer can re-apply if their circumstances change.Keep declines respectful and never expose the underlying assessment data. See how to apply and the eligibility criteria. In practice Treat out_of_appetite as one branch of your reason handling, not the who"},{"t":"Design principles of the public API","u":"/public-api/design-principles-of-the-public-api/","c":"Public API guides","e":"Public API","s":"The public API follows five principles you can rely on. It is keyless by default, keeps all per-customer PII private, returns server-fixed response shapes, changes only additively within v1, and degrades gracefully when a feature is off. Understanding them lets you write an integration that stays correct as the platform evolves.","b":"Keyless by default Nothing on the public ring is per-caller, so there is no key or token to manage. Protection comes from rate limiting, validation and fixed shapes instead. See authentication on the public ring. PII stays private No per-customer personal data ever crosses the ring. Responses are public vocabulary or public-safe projections; sensitive fields are structurally absent, not masked. See Privacy and PII. Server-fixed shapes The server sets privileged fields itself — an enquiry’s status, a biller response’s omitted bank data — so a caller can never smuggle in a value it should not co"},{"t":"Designing a reliable webhook consumer","u":"/public-api/designing-a-reliable-webhook-consumer/","c":"Public API guides","e":"Public API","s":"A reliable webhook consumer does four things: verify, acknowledge fast, dedupe, and reconcile. Verify the signature over the raw body, return 200 within 10 seconds, deduplicate on the event id, and treat delivery as unordered. Get these right and webhooks become boringly dependable.","b":"The four properties Verify — HMAC over the raw body, fresh timestamp. See signature verification.Acknowledge fast — 200 within the 10-second timeout; process asynchronously.Deduplicate — on the event id, because delivery is at-least-once. See idempotent handlers.Reconcile — treat events as unordered; trust created and live state. Put together The verify-then-enqueue pattern satisfies all four cleanly: verify and enqueue synchronously (fast ack), then dedupe and reconcile in the worker. It is the shape to reach for at any real volume. Which property is most often skipped? Idempotency. Teams ver"},{"t":"Developer changelog","u":"/glossary/changelog/","c":"Developer glossary","e":"Glossary","s":"The changelog is the running record of API changes — additions within a version and advance notice of deprecations. Watch it to keep an integration current.","b":"Definition The developer changelog lists what changed and when: new endpoints and fields (additive, within a version), and deprecations with their sunset dates. It is the authoritative feed for versioning and deprecation. In plain terms The list of what changed in the API, so you are never surprised. Why it matters here Treat any deprecation entry as a dated task. See versioning and deprecation policy."},{"t":"Diagnose a 401 or 403","u":"/recipes/diagnose-a-401-or-403/","c":"Integration recipes","e":"Recipe","s":"401 and 403 are different failures. A 401 means your credential was missing or invalid — the platform does not know who you are. A 403 means it knows you, but you are not allowed to do this. The fix differs accordingly.","b":"Is it a 401? The credential is missing, expired or malformed. Re-issue the API key or refresh the OAuth token and attach it correctly. See missing_api_key and invalid_api_key. Is it a 403? Your credential is valid but lacks the scope, or the resource belongs to another account. Request a token with the right scope. See insufficient_scope. Do not retry the same credential Neither is transient. Retrying the identical token reproduces the identical error. Fix auth first, then call again. Why is the public ring not affected? The `/public/v1` ring is unauthenticated — there is no credential to be w"},{"t":"Edge (server-side client)","u":"/glossary/edge/","c":"Developer glossary","e":"Glossary","s":"Edge (server-side client) — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is The edge is a trusted server-side layer in front of your users — a web server, function, or gateway — that can validate input, hold secrets and forward requests to an API on the browser’s behalf. In the Credicorp API The Credicorp consent endpoint treats your edge as the trust boundary: it validates the visitor’s cookie-banner interaction, then forwards the snapshot server-side, so the hub can accept it without a CSRF check. Using an edge also lets you cache reads and apply your own rate limiting. Why forward through an edge instead of the browser? Because your edge can validate the"},{"t":"Embed a Credicorp comparison listing","u":"/recipes/embed-a-comparison-listing/","c":"Integration recipes","e":"Recipe","s":"Render an accurate Credicorp listing on a comparison site from two public reads — products and pricing — plus a live quote. No token, and the figures track the source automatically.","b":"Read the catalogue and figures List the products from GET /public/v1/products (respecting each product's enabled flag) and pull published figures from config/pricing. Because these are the single source the marketing estate reads, your listing never drifts from the real numbers — do not hard-code figures. Add a live quote For the Business Loan, show a representative figure from the quote endpoint for the user's amount and term (clamped to £50–£500 / 14–84 days). For Flex and Slice, present the published framing from product_details. Attribute and hand off Credicorp's ai.txt asks for attributio"},{"t":"Embed the Credicorp support widget","u":"/recipes/embed-the-support-widget/","c":"Integration recipes","e":"Recipe","s":"Add live support to any page with two tags. Load the widget stylesheet in the head and the widget script with defer, and the Credicorp support widget mounts itself and talks to the chat endpoint for you. There is no build step, no dependency, and no API key.","b":"Step 1 — add the assets &lt;link rel=\"stylesheet\" href=\"https://hub.credicorp.co.uk/public/v1/support/widget.css\"&gt; &lt;script src=\"https://hub.credicorp.co.uk/public/v1/support/widget.js\" defer&gt;&lt;/script&gt; Step 2 — that is it The script mounts the widget and wires it to POST /public/v1/support/chat. You do not manage sessions or transport yourself. Caching the assets The CSS and JS are static — let your CDN and the browser cache them aggressively. See Caching. Being a good citizen The chat endpoint is rate limited by IP (60/60 s). The widget debounces input, but if you script against"},{"t":"Enabled flag","u":"/glossary/enabled-flag/","c":"Developer glossary","e":"Glossary","s":"The enabled flag is a per-product boolean on the products endpoint. Render availability from it — only quote and hand off products that are currently enabled.","b":"Definition Each entry in the products list carries an enabled boolean indicating whether the product is currently available to apply for. It lets you show accurate availability rather than assuming every listed product can be taken right now. In plain terms A yes/no on each product for whether people can apply for it today. Why it matters here Respect it in comparison listings and assistants — do not hand a user toward a disabled product. Related to the feature flag concept."},{"t":"Endpoint","u":"/glossary/api-endpoint/","c":"Developer glossary","e":"Glossary","s":"Endpoint — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is An endpoint is a single addressable operation in an API — a URL path paired with an HTTP method, such as GET /public/v1/loyalty/tiers. Each endpoint has a defined request shape, response and set of possible errors. In the Credicorp API The Credicorp API reference documents one page per public endpoint: its method, path, parameters, success response and errors. Browse them to see the full public surface at a glance. How many public endpoints are there? A small, stable set on the `/public/v1` ring — enquiries, consent, loyalty tiers, CMS pages, Slice billers, support chat and widget a"},{"t":"Environments and the sandbox","u":"/public-api/environments-and-sandbox/","c":"Public API guides","e":"Partner API","s":"Partner integrations get an independent sandbox project with its own OAuth client and its own rate-limit bucket. The public ring has no sandbox — it only ever returns published, non-sensitive data.","b":"Sandbox and live are separate projects Each partner has a sandbox project and a live project. They carry different OAuth clients, so a sandbox token can never reach live data and vice versa, and they have independent rate-limit buckets — load-testing the sandbox never eats into your production allowance. Sandbox is fixed at the build tier (10 req/s, burst 50) regardless of your live tier, which is deliberate: sandbox is for correctness, live is for throughput. Why the public ring has no sandbox The public ring returns only published figures — there is no customer data, no decision, no money — "},{"t":"Error code","u":"/glossary/error-code/","c":"Developer glossary","e":"Glossary","s":"An error code is the stable, machine-readable string in the error envelope (error.code) that you branch on — distinct from the human message, which may change.","b":"Definition An error code is the part of the error envelope designed for code to read. Unlike the HTTP status (broad class) and the message (human, may be reworded), the code is a stable contract: consent_required always means the same thing. Branch on it, look unfamiliar ones up in the catalogue, and map each to your own user-facing copy. Code or status — which do I branch on? Status for retry-or-not; code for the specific behaviour. Both, in that order."},{"t":"Error code catalogue","u":"/reference/error-code-catalogue/","c":"API reference","e":"API reference","s":"This is the master list of error.code values. Each links to a dedicated page with the exact cause, an example response and the fix. Branch your integration on code — it is stable — rather than on the human message.","b":"All error codes Follow any code for its dedicated reference.CodeStatusMeaninginvalid_json400The request body was not valid JSON.missing_content_type400The request was missing a JSON content type.unknown_field400The body contained a field the endpoint does not accept.form_required422The required form key was absent.consent_required422The consent gate was not satisfied.invalid_email422A supplied email address was not valid.payload_too_large422The fields object exceeded the size limit.invalid_dept422The routing department was not recognised.invalid_value_type422A field value was the wrong type.id"},{"t":"Error code: account_suspended","u":"/reference/error-account-suspended/","c":"API reference","e":"API reference","s":"account_suspended returns HTTP 403 with type: authentication_error. The partner account is suspended. Branch on the code and apply the fix below.","b":"What triggers it The partner account is suspended. An account paused for compliance or billing reasons. Example response { \"error\": { \"type\": \"authentication_error\", \"code\": \"account_suspended\", \"message\": \"The partner account is suspended.\" } } How to fix it Contact partner support to resolve the suspension.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, account_suspended is handled by branching on error.code rather than on the human error.message, which may be reworded over time. The HTTP status (403) "},{"t":"Error code: audience_mismatch","u":"/reference/error-audience-mismatch/","c":"API reference","e":"API reference","s":"audience_mismatch returns HTTP 401 with type: authentication_error. The token audience did not match this API. Branch on the code and apply the fix below.","b":"What triggers it The token audience did not match this API. A token minted for a different resource server. Example response { \"error\": { \"type\": \"authentication_error\", \"code\": \"audience_mismatch\", \"message\": \"The token audience did not match this API.\" } } How to fix it Request a token scoped to the Credicorp partner API audience.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, audience_mismatch is handled by branching on error.code rather than on the human error.message, which may be reworded over time"},{"t":"Error code: cms_page_not_found","u":"/reference/error-cms-page-not-found/","c":"API reference","e":"API reference","s":"cms_page_not_found returns HTTP 404 with type: invalid_request_error. The requested CMS page key does not exist. When it is about one field, error.param names key. Branch on the code and apply the fix below.","b":"What triggers it The requested CMS page key does not exist. An unknown {key} on GET /public/v1/cms/pages/{key}. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"cms_page_not_found\", \"message\": \"The requested CMS page key does not exist.\", \"param\": \"key\" } } How to fix it Use a published page key; keys are lower-case slugs.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, cms_page_not_found is handled by branching on error.code rather than on the human error.message, which may be rewo"},{"t":"Error code: consent_required","u":"/reference/error-consent-required/","c":"API reference","e":"API reference","s":"consent_required returns HTTP 422 with type: invalid_request_error. The consent gate was not satisfied. When it is about one field, error.param points at fields.consent. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it The consent gate was not satisfied. fields.consent missing or not equal to the string \"yes\". Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"consent_required\", \"message\": \"The consent gate was not satisfied.\", \"param\": \"fields.consent\" } } How to fix it Collect explicit consent and send fields.consent: \"yes\".This is deterministic: the same request will fail again until fixed. See the HTTP 422 page for the class. In practice In a well-built client, consent_required is handled by branching on error.code rather than on the human error.message, which may be"},{"t":"Error code: form_required","u":"/reference/error-form-required/","c":"API reference","e":"API reference","s":"form_required returns HTTP 422 with type: invalid_request_error. The required <code>form</code> key was absent. When it is about one field, error.param points at form. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it The required form key was absent. Submitting an enquiry without identifying which form it came from. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"form_required\", \"message\": \"The required form key was absent.\", \"param\": \"form\" } } How to fix it Set form to the forms-registry key (lower-case slug, 1–64 chars).This is deterministic: the same request will fail again until fixed. See the HTTP 422 page for the class. In practice In a well-built client, form_required is handled by branching on error.code rather than on the human error.message, which may be "},{"t":"Error code: idempotency_key_in_progress","u":"/reference/error-idempotency-key-in-progress/","c":"API reference","e":"API reference","s":"idempotency_key_in_progress returns HTTP 409 with type: invalid_request_error. A request with this key is still being processed. When it is about one field, error.param names Idempotency-Key. Branch on the code and apply the fix below.","b":"What triggers it A request with this key is still being processed. A retry landed while the original request under the same key is still in flight. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"idempotency_key_in_progress\", \"message\": \"A request with this key is still being processed.\", \"param\": \"Idempotency-Key\" } } How to fix it Wait and retry; the original will complete and its result becomes replayable.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, idempotency_key_in_progre"},{"t":"Error code: idempotency_key_malformed","u":"/reference/error-idempotency-key-malformed/","c":"API reference","e":"API reference","s":"idempotency_key_malformed returns HTTP 400 with type: invalid_request_error. The idempotency key was not a valid format. When it is about one field, error.param names Idempotency-Key. Branch on the code and apply the fix below.","b":"What triggers it The idempotency key was not a valid format. An empty key, or one over 255 characters. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"idempotency_key_malformed\", \"message\": \"The idempotency key was not a valid format.\", \"param\": \"Idempotency-Key\" } } How to fix it Send a non-empty key up to 255 chars — a UUID is ideal.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, idempotency_key_malformed is handled by branching on error.code rather than on the human error.messa"},{"t":"Error code: idempotency_key_reused","u":"/reference/error-idempotency-key-reused/","c":"API reference","e":"API reference","s":"idempotency_key_reused returns HTTP 409 with type: invalid_request_error. An idempotency key was reused with a different body. When it is about one field, error.param points at Idempotency-Key. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it An idempotency key was reused with a different body. Sending a new request body under a key already used for a different one. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"idempotency_key_reused\", \"message\": \"An idempotency key was reused with a different body.\", \"param\": \"Idempotency-Key\" } } How to fix it Use a fresh key per distinct operation; reuse the same key only for identical retries.This is deterministic: the same request will fail again until fixed. See the HTTP 409 page for the class. In practice In a well-built client, idempotency_key_reus"},{"t":"Error code: insufficient_scope","u":"/reference/error-insufficient-scope/","c":"API reference","e":"API reference","s":"insufficient_scope returns HTTP 403 with type: authentication_error. The token lacks the required scope. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it The token lacks the required scope. A valid token whose granted scopes do not cover this action. Example response { \"error\": { \"type\": \"authentication_error\", \"code\": \"insufficient_scope\", \"message\": \"The token lacks the required scope.\" } } How to fix it Request a token with the needed scope from the partner console.This is deterministic: the same request will fail again until fixed. See the HTTP 403 page for the class. In practice In a well-built client, insufficient_scope is handled by branching on error.code rather than on the human error.message, which may be reworded ove"},{"t":"Error code: invalid_api_key","u":"/reference/error-invalid-api-key/","c":"API reference","e":"API reference","s":"invalid_api_key returns HTTP 401 with type: authentication_error. The supplied credential was not valid. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it The supplied credential was not valid. A revoked, expired or mistyped API key or token. Example response { \"error\": { \"type\": \"authentication_error\", \"code\": \"invalid_api_key\", \"message\": \"The supplied credential was not valid.\" } } How to fix it Reissue or re-check the credential; do not retry the same one.This is deterministic: the same request will fail again until fixed. See the HTTP 401 page for the class. In practice In a well-built client, invalid_api_key is handled by branching on error.code rather than on the human error.message, which may be reworded over time. The H"},{"t":"Error code: invalid_dept","u":"/reference/error-invalid-dept/","c":"API reference","e":"API reference","s":"invalid_dept returns HTTP 422 with type: invalid_request_error. The routing department was not recognised. When it is about one field, error.param points at dept. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it The routing department was not recognised. A dept value outside support|payments|hardship|complaints. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"invalid_dept\", \"message\": \"The routing department was not recognised.\", \"param\": \"dept\" } } How to fix it Use one of the four routing values, or omit dept for the general inbox.This is deterministic: the same request will fail again until fixed. See the HTTP 422 page for the class. In practice In a well-built client, invalid_dept is handled by branching on error.code rather than on the human error.message,"},{"t":"Error code: invalid_email","u":"/reference/error-invalid-email/","c":"API reference","e":"API reference","s":"invalid_email returns HTTP 422 with type: invalid_request_error. A supplied email address was not valid. When it is about one field, error.param points at fields.email. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it A supplied email address was not valid. A malformed fields.email value. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"invalid_email\", \"message\": \"A supplied email address was not valid.\", \"param\": \"fields.email\" } } How to fix it Validate the address client-side, or omit the field if you have no email.This is deterministic: the same request will fail again until fixed. See the HTTP 422 page for the class. In practice In a well-built client, invalid_email is handled by branching on error.code rather than on the human error.message, which may be reworde"},{"t":"Error code: invalid_json","u":"/reference/error-invalid-json/","c":"API reference","e":"API reference","s":"invalid_json returns HTTP 400 with type: invalid_request_error. The request body was not valid JSON. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it The request body was not valid JSON. A truncated payload, a trailing comma, or a body sent without a JSON content type. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"invalid_json\", \"message\": \"The request body was not valid JSON.\" } } How to fix it Serialise with a real JSON encoder and set Content-Type: application/json.This is deterministic: the same request will fail again until fixed. See the HTTP 400 page for the class. In practice In a well-built client, invalid_json is handled by branching on error.code rather than on the human error.message, w"},{"t":"Error code: invalid_value_type","u":"/reference/error-invalid-value-type/","c":"API reference","e":"API reference","s":"invalid_value_type returns HTTP 422 with type: invalid_request_error. A field value was the wrong type. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it A field value was the wrong type. A nested object or array inside fields, which only accepts scalar values. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"invalid_value_type\", \"message\": \"A field value was the wrong type.\" } } How to fix it Flatten the value to a string, number or boolean.This is deterministic: the same request will fail again until fixed. See the HTTP 422 page for the class. In practice In a well-built client, invalid_value_type is handled by branching on error.code rather than on the human error.message, which may be reworded over ti"},{"t":"Error code: mcp_invalid_params","u":"/reference/error-mcp-invalid-params/","c":"API reference","e":"API reference","s":"mcp_invalid_params returns HTTP 422 with type: invalid_request_error. MCP tool parameters failed validation. When it is about one field, error.param names params. Branch on the code and apply the fix below.","b":"What triggers it MCP tool parameters failed validation. Missing or wrong-typed arguments in a tools/call. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"mcp_invalid_params\", \"message\": \"MCP tool parameters failed validation.\", \"param\": \"params\" } } How to fix it Match the tool’s input schema exactly.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, mcp_invalid_params is handled by branching on error.code rather than on the human error.message, which may be reworded over time. The H"},{"t":"Error code: mcp_method_not_found","u":"/reference/error-mcp-method-not-found/","c":"API reference","e":"API reference","s":"mcp_method_not_found returns HTTP 404 with type: invalid_request_error. An MCP JSON-RPC method was not recognised. When it is about one field, error.param names method. Branch on the code and apply the fix below.","b":"What triggers it An MCP JSON-RPC method was not recognised. Calling a method the MCP server does not implement. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"mcp_method_not_found\", \"message\": \"An MCP JSON-RPC method was not recognised.\", \"param\": \"method\" } } How to fix it Use a method the server advertises; call tools/list to discover them.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, mcp_method_not_found is handled by branching on error.code rather than on the human error.me"},{"t":"Error code: mcp_tool_not_found","u":"/reference/error-mcp-tool-not-found/","c":"API reference","e":"API reference","s":"mcp_tool_not_found returns HTTP 404 with type: invalid_request_error. An MCP tool name was not recognised. When it is about one field, error.param names name. Branch on the code and apply the fix below.","b":"What triggers it An MCP tool name was not recognised. A typo in the tool name in a tools/call request. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"mcp_tool_not_found\", \"message\": \"An MCP tool name was not recognised.\", \"param\": \"name\" } } How to fix it List tools first and call one that exists.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, mcp_tool_not_found is handled by branching on error.code rather than on the human error.message, which may be reworded over time. The HTTP"},{"t":"Error code: method_not_allowed","u":"/reference/error-method-not-allowed/","c":"API reference","e":"API reference","s":"method_not_allowed returns HTTP 405 with type: invalid_request_error. The HTTP method is not allowed on this route. Branch on the code and apply the fix below.","b":"What triggers it The HTTP method is not allowed on this route. Using PUT/DELETE where only GET or POST is defined. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"method_not_allowed\", \"message\": \"The HTTP method is not allowed on this route.\" } } How to fix it Use the method the endpoint reference documents.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, method_not_allowed is handled by branching on error.code rather than on the human error.message, which may be reworded over time"},{"t":"Error code: missing_api_key","u":"/reference/error-missing-api-key/","c":"API reference","e":"API reference","s":"missing_api_key returns HTTP 401 with type: authentication_error. A partner-plane call had no API key. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it A partner-plane call had no API key. Calling /partner/v1 without an Authorization credential. Example response { \"error\": { \"type\": \"authentication_error\", \"code\": \"missing_api_key\", \"message\": \"A partner-plane call had no API key.\" } } How to fix it Attach a valid partner API key or OAuth bearer token.This is deterministic: the same request will fail again until fixed. See the HTTP 401 page for the class. In practice In a well-built client, missing_api_key is handled by branching on error.code rather than on the human error.message, which may be reworded over time. The HTTP s"},{"t":"Error code: missing_content_type","u":"/reference/error-missing-content-type/","c":"API reference","e":"API reference","s":"missing_content_type returns HTTP 400 with type: invalid_request_error. The request was missing a JSON content type. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it The request was missing a JSON content type. Posting a body without Content-Type: application/json. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"missing_content_type\", \"message\": \"The request was missing a JSON content type.\" } } How to fix it Add the header. The API will not guess the body format.This is deterministic: the same request will fail again until fixed. See the HTTP 400 page for the class. In practice In a well-built client, missing_content_type is handled by branching on error.code rather than on the human error.message, which may be rew"},{"t":"Error code: payload_too_large","u":"/reference/error-payload-too-large/","c":"API reference","e":"API reference","s":"payload_too_large returns HTTP 422 with type: invalid_request_error. The fields object exceeded the size limit. When it is about one field, error.param points at fields. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it The fields object exceeded the size limit. The encoded fields object was larger than 16 KiB, or had more than 60 keys. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"payload_too_large\", \"message\": \"The fields object exceeded the size limit.\", \"param\": \"fields\" } } How to fix it Trim the payload — move large content out of the enquiry fields.This is deterministic: the same request will fail again until fixed. See the HTTP 422 page for the class. In practice In a well-built client, payload_too_large is handled by branching on error.code rather than on th"},{"t":"Error code: rate_limited","u":"/reference/error-rate-limited/","c":"API reference","e":"API reference","s":"rate_limited returns HTTP 429 with type: rate_limit_error. You exceeded the request rate. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it You exceeded the request rate. More than 60 requests per 60 seconds per IP on the public ring. Example response { \"error\": { \"type\": \"rate_limit_error\", \"code\": \"rate_limited\", \"message\": \"You exceeded the request rate.\" } } How to fix it Back off until Retry-After elapses; batch or cache where you can.This class is transient — see Retrying failed requests. In practice In a well-built client, rate_limited is handled by branching on error.code rather than on the human error.message, which may be reworded over time. The HTTP status (429) gives the broad rate_limit_error class; t"},{"t":"Error code: request_body_too_large","u":"/reference/error-request-body-too-large/","c":"API reference","e":"API reference","s":"request_body_too_large returns HTTP 422 with type: invalid_request_error. The whole request body exceeded the size cap. Branch on the code and apply the fix below.","b":"What triggers it The whole request body exceeded the size cap. A payload larger than the endpoint’s maximum (e.g. 16 KiB on enquiries). Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"request_body_too_large\", \"message\": \"The whole request body exceeded the size cap.\" } } How to fix it Reduce the payload; move large content out of the request.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, request_body_too_large is handled by branching on error.code rather than on the human error.m"},{"t":"Error code: resource_not_found","u":"/reference/error-resource-not-found/","c":"API reference","e":"API reference","s":"resource_not_found returns HTTP 404 with type: invalid_request_error. The referenced resource does not exist. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it The referenced resource does not exist. An id that never existed, was deleted, or belongs to another environment. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"resource_not_found\", \"message\": \"The referenced resource does not exist.\" } } How to fix it Check the id and that you are calling the right (live vs test) environment.This is deterministic: the same request will fail again until fixed. See the HTTP 404 page for the class. In practice In a well-built client, resource_not_found is handled by branching on error.code rather than on the human error."},{"t":"Error code: route_not_found","u":"/reference/error-route-not-found/","c":"API reference","e":"API reference","s":"route_not_found returns HTTP 404 with type: invalid_request_error. No endpoint matches the path. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it No endpoint matches the path. A misspelled path or a missing API version segment. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"route_not_found\", \"message\": \"No endpoint matches the path.\" } } How to fix it Confirm the full path including /public/v1 or /partner/v1.This is deterministic: the same request will fail again until fixed. See the HTTP 404 page for the class. In practice In a well-built client, route_not_found is handled by branching on error.code rather than on the human error.message, which may be reworded over time. The HTTP status (404) g"},{"t":"Error code: scope_not_granted","u":"/reference/error-scope-not-granted/","c":"API reference","e":"API reference","s":"scope_not_granted returns HTTP 403 with type: authentication_error. The token is missing a specific required scope. Branch on the code and apply the fix below.","b":"What triggers it The token is missing a specific required scope. A valid token whose scopes do not include the one this route needs. Example response { \"error\": { \"type\": \"authentication_error\", \"code\": \"scope_not_granted\", \"message\": \"The token is missing a specific required scope.\" } } How to fix it Request the named scope from the partner console and re-mint the token.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, scope_not_granted is handled by branching on error.code rather than on the human error."},{"t":"Error code: server_error","u":"/reference/error-server-error/","c":"API reference","e":"API reference","s":"server_error returns HTTP 500 with type: api_error. An unexpected server-side error occurred. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it An unexpected server-side error occurred. A transient platform fault unrelated to your request. Example response { \"error\": { \"type\": \"api_error\", \"code\": \"server_error\", \"message\": \"An unexpected server-side error occurred.\" } } How to fix it Retry with backoff; if it persists, contact support quoting the request id.This class is transient — see Retrying failed requests. In practice In a well-built client, server_error is handled by branching on error.code rather than on the human error.message, which may be reworded over time. The HTTP status (500) gives the broad api_error "},{"t":"Error code: signature_invalid","u":"/reference/error-signature-invalid/","c":"API reference","e":"API reference","s":"signature_invalid returns HTTP 400 with type: invalid_request_error. A request-signed call failed signature verification. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it A request-signed call failed signature verification. A wrong signing key, a modified body, or a clock skew beyond tolerance. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"signature_invalid\", \"message\": \"A request-signed call failed signature verification.\" } } How to fix it Recompute the signature over the raw body with the correct key; check the timestamp.This is deterministic: the same request will fail again until fixed. See the HTTP 400 page for the class. In practice In a well-built client, signature_invalid is handled by branching on error.code "},{"t":"Error code: support_message_too_long","u":"/reference/error-support-message-too-long/","c":"API reference","e":"API reference","s":"support_message_too_long returns HTTP 422 with type: invalid_request_error. A support chat message exceeded the length limit. When it is about one field, error.param names message. Branch on the code and apply the fix below.","b":"What triggers it A support chat message exceeded the length limit. A message body over the per-message character limit. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"support_message_too_long\", \"message\": \"A support chat message exceeded the length limit.\", \"param\": \"message\" } } How to fix it Split the message or trim it under the limit.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, support_message_too_long is handled by branching on error.code rather than on the human error.me"},{"t":"Error code: support_session_closed","u":"/reference/error-support-session-closed/","c":"API reference","e":"API reference","s":"support_session_closed returns HTTP 409 with type: invalid_request_error. The support chat session is already closed. When it is about one field, error.param names session_id. Branch on the code and apply the fix below.","b":"What triggers it The support chat session is already closed. Posting to a session that has ended or escalated. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"support_session_closed\", \"message\": \"The support chat session is already closed.\", \"param\": \"session_id\" } } How to fix it Start a new session rather than reusing a closed one.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, support_session_closed is handled by branching on error.code rather than on the human error.message, w"},{"t":"Error code: temporarily_unavailable","u":"/reference/error-temporarily-unavailable/","c":"API reference","e":"API reference","s":"temporarily_unavailable returns HTTP 503 with type: api_error. The service is briefly unavailable. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it The service is briefly unavailable. Maintenance or an overloaded dependency. Example response { \"error\": { \"type\": \"api_error\", \"code\": \"temporarily_unavailable\", \"message\": \"The service is briefly unavailable.\" } } How to fix it Retry with backoff; the service returns shortly.This class is transient — see Retrying failed requests. In practice In a well-built client, temporarily_unavailable is handled by branching on error.code rather than on the human error.message, which may be reworded over time. The HTTP status (503) gives the broad api_error class; the code gives the spec"},{"t":"Error code: token_expired","u":"/reference/error-token-expired/","c":"API reference","e":"API reference","s":"token_expired returns HTTP 401 with type: authentication_error. The OAuth access token has expired. Branch on the code and apply the fix below.","b":"What triggers it The OAuth access token has expired. Using a bearer token past its lifetime on the partner plane. Example response { \"error\": { \"type\": \"authentication_error\", \"code\": \"token_expired\", \"message\": \"The OAuth access token has expired.\" } } How to fix it Refresh the token and retry with the new one.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, token_expired is handled by branching on error.code rather than on the human error.message, which may be reworded over time. The HTTP status (401) g"},{"t":"Error code: token_revoked","u":"/reference/error-token-revoked/","c":"API reference","e":"API reference","s":"token_revoked returns HTTP 401 with type: authentication_error. The credential was revoked. Branch on the code and apply the fix below.","b":"What triggers it The credential was revoked. An API key or token that has been explicitly revoked. Example response { \"error\": { \"type\": \"authentication_error\", \"code\": \"token_revoked\", \"message\": \"The credential was revoked.\" } } How to fix it Issue a new credential from the partner console.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, token_revoked is handled by branching on error.code rather than on the human error.message, which may be reworded over time. The HTTP status (401) gives the broad authe"},{"t":"Error code: unknown_field","u":"/reference/error-unknown-field/","c":"API reference","e":"API reference","s":"unknown_field returns HTTP 400 with type: invalid_request_error. The body contained a field the endpoint does not accept. Branch on this code — it is stable — and apply the fix below.","b":"What triggers it The body contained a field the endpoint does not accept. A typo in a field name or a field copied from a different endpoint. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"unknown_field\", \"message\": \"The body contained a field the endpoint does not accept.\" } } How to fix it Remove or correct the field; check the endpoint reference for accepted keys.This is deterministic: the same request will fail again until fixed. See the HTTP 400 page for the class. In practice In a well-built client, unknown_field is handled by branching on error.code rather than "},{"t":"Error code: unsupported_media_type","u":"/reference/error-unsupported-media-type/","c":"API reference","e":"API reference","s":"unsupported_media_type returns HTTP 415 with type: invalid_request_error. The content type is not supported. Branch on the code and apply the fix below.","b":"What triggers it The content type is not supported. A body sent as something other than application/json. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"unsupported_media_type\", \"message\": \"The content type is not supported.\" } } How to fix it Send JSON with Content-Type: application/json.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, unsupported_media_type is handled by branching on error.code rather than on the human error.message, which may be reworded over time. The HTTP sta"},{"t":"Error code: webhook_endpoint_limit","u":"/reference/error-webhook-endpoint-limit/","c":"API reference","e":"API reference","s":"webhook_endpoint_limit returns HTTP 422 with type: invalid_request_error. The endpoint limit was reached. Branch on the code and apply the fix below.","b":"What triggers it The endpoint limit was reached. Trying to register more than 20 endpoints on one partner account. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"webhook_endpoint_limit\", \"message\": \"The endpoint limit was reached.\" } } How to fix it Reuse or delete an existing endpoint before adding another.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, webhook_endpoint_limit is handled by branching on error.code rather than on the human error.message, which may be reworded over"},{"t":"Error code: webhook_endpoint_url_invalid","u":"/reference/error-webhook-endpoint-url-invalid/","c":"API reference","e":"API reference","s":"webhook_endpoint_url_invalid returns HTTP 422 with type: invalid_request_error. A webhook endpoint URL was rejected. When it is about one field, error.param names url. Branch on the code and apply the fix below.","b":"What triggers it A webhook endpoint URL was rejected. A non-HTTPS URL, an unresolvable host, or a private/loopback address. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"webhook_endpoint_url_invalid\", \"message\": \"A webhook endpoint URL was rejected.\", \"param\": \"url\" } } How to fix it Register a publicly reachable HTTPS URL with a valid certificate.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, webhook_endpoint_url_invalid is handled by branching on error.code rather than on the"},{"t":"Error code: webhook_event_unknown","u":"/reference/error-webhook-event-unknown/","c":"API reference","e":"API reference","s":"webhook_event_unknown returns HTTP 422 with type: invalid_request_error. An unknown event type was subscribed to. When it is about one field, error.param names enabled_events. Branch on the code and apply the fix below.","b":"What triggers it An unknown event type was subscribed to. A typo in enabled_events or a type not in the catalogue. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"webhook_event_unknown\", \"message\": \"An unknown event type was subscribed to.\", \"param\": \"enabled_events\" } } How to fix it Use a type from the event catalogue, or *.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, webhook_event_unknown is handled by branching on error.code rather than on the human error.message, which may"},{"t":"Error code: webhook_signature_invalid","u":"/reference/error-webhook-signature-invalid/","c":"API reference","e":"API reference","s":"webhook_signature_invalid returns HTTP 400 with type: invalid_request_error. A webhook signature failed verification. Branch on the code and apply the fix below.","b":"What triggers it A webhook signature failed verification. A wrong signing secret, a re-serialised body, or a timestamp outside the 300-second tolerance. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"webhook_signature_invalid\", \"message\": \"A webhook signature failed verification.\" } } How to fix it Verify over the raw body with the correct secret; check for clock skew.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, webhook_signature_invalid is handled by branching on error.code r"},{"t":"Error code: webhook_signature_missing","u":"/reference/error-webhook-signature-missing/","c":"API reference","e":"API reference","s":"webhook_signature_missing returns HTTP 400 with type: invalid_request_error. A webhook arrived without a signature header. Branch on the code and apply the fix below.","b":"What triggers it A webhook arrived without a signature header. The Credicorp-Signature header was absent — usually a proxy stripping headers. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"webhook_signature_missing\", \"message\": \"A webhook arrived without a signature header.\" } } How to fix it Reject the request; ensure your proxy forwards the Credicorp-Signature header intact.This is deterministic; fix the cause before resending. See the error code catalogue for related codes. In practice In a well-built client, webhook_signature_missing is handled by branching on erro"},{"t":"Errors and status codes on the public API","u":"/public-api/errors-and-status-codes/","c":"Public API guides","e":"Public API","s":"Every public-API response uses standard HTTP status codes and a consistent error envelope. A read returns 200, a successful enquiry returns 201, malformed JSON returns 400, an unknown key returns 404, a validation failure returns 422, and a rate-limit overflow returns 429. Errors always carry a machine-readable code and a human message so you can branch on the code and log the message.","b":"The status codes you will see CodeMeaningTypical cause200OKA successful read (loyalty tiers, CMS page, billers, MCP).201CreatedAn enquiry was recorded.400Bad RequestMalformed JSON or a structurally invalid body.404Not FoundAn unknown CMS key, or an unpublished page.422Unprocessable EntityA well-formed request that fails validation — missing consent, oversized fields, bad email.429Too Many RequestsThe per-IP window was exceeded; retry after Retry-After. The error envelope Every non-2xx response carries the same shape: a top-level error object with a stable code and a human-readable message.{ \"e"},{"t":"Errors, status codes and safe retries","u":"/public-api/errors-and-retries/","c":"Public API guides","e":"Reliability","s":"Errors come back as JSON with a stable machine code and a human message. 4xx means fix the request; 429/5xx are retryable with back-off. Pair retries with an idempotency key so a replay never double-acts.","b":"The error body Every error — on either ring — returns a JSON object with a machine-readable code, a human message, and where relevant a details array pinpointing the offending field. Branch on code, not on the message text, which may be reworded. The HTTP status tells you the family; the code tells you the specific fault. See the full tables for the public ring and partner ring. Which errors to retry StatusMeaningRetry?400 / 422Bad or invalid requestNo — fix the request401 / 403Auth / scope problemNo — fix the credential404Unknown resourceNo409Conflict (e.g. duplicate)No — reconcile first429Ra"},{"t":"Event envelope","u":"/glossary/event-envelope/","c":"Developer glossary","e":"Glossary","s":"The event envelope is the consistent outer JSON every webhook shares — id, type, created, livemode, api_version and the data payload — so one handler can route on type.","b":"Definition The event envelope is the fixed wrapper around every Credicorp webhook. Its metadata identifies the event and its version; the resource itself lives under data.object. Because the envelope shape never varies across event types, you can write one dispatcher that reads type and hands data.object to the right handler. See the envelope reference. What is the difference between the envelope and the payload? The envelope is the metadata wrapper (id, type, created…); the payload is the resource under `data.object`. See [event payload](/glossary/event-payload/)."},{"t":"Event payload","u":"/glossary/event-payload/","c":"Developer glossary","e":"Glossary","s":"The event payload is the data a webhook carries — in Credicorp’s envelope it lives under data.object as a snapshot of the resource the event is about.","b":"Definition The event payload is the meaningful content of a webhook, distinct from the envelope metadata (id, type, created). Credicorp places it at data.object, shaped exactly like the resource’s API representation and snapshotted when the event fired. If the resource may have changed since, re-fetch it from the read API for the current state. Is the payload the live resource? It is a snapshot at event time. For authoritative current state, re-read the resource from its endpoint. See [the event envelope](/reference/webhook-event-envelope/)."},{"t":"Exponential back-off","u":"/glossary/exponential-back-off/","c":"Developer glossary","e":"Glossary","s":"Exponential back-off — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is Exponential back-off spaces out retries by doubling the wait each time — 1s, 2s, 4s, and so on — so a struggling server is not swamped by immediate re-tries. It is usually capped at a small number of attempts before giving up. When to use it Use exponential back-off for transient 5xx errors and network blips. For a 429, honour the exact Retry-After value instead of guessing. Do not retry deterministic 4xx errors like 422 — they will fail identically. See handling rate limits gracefully. Should I use back-off for a 429? Honour the Retry-After header for a 429 rather than back-off. Us"},{"t":"Expose Credicorp tools to Claude via MCP","u":"/recipes/expose-credicorp-tools-to-claude/","c":"Integration recipes","e":"Recipe","s":"Point a Claude agent at the public MCP server and it can quote, qualify and guide from live Credicorp figures. Register the server, list tools, and instruct the model to answer only from tool results.","b":"Register the server Claude speaks MCP natively, so you add the Credicorp public server as a tool source pointing at /public/v1/mcp. No credential is needed for the public server. On connect, the client runs initialize and tools/list and the six tools become available to the model. Ground the answers Instruct the model to answer questions about Credicorp products, cost and eligibility only from tool results — get_quote, product_details, eligibility_criteria. Because the tools return the hub's authoritative figures, a tool-grounded answer is accurate where a from-memory answer would not be. Hand"},{"t":"Fail-open","u":"/glossary/fail-open/","c":"Developer glossary","e":"Glossary","s":"Fail-open means an endpoint returns a soft acknowledgement on an internal failure rather than blocking the caller. Credicorp's consent and enquiries endpoints are fail-open by design.","b":"Definition A fail-open endpoint prioritises not blocking the user over guaranteeing the write. If storage momentarily fails, it acknowledges rather than erroring, so the visitor's journey is never held up. Credicorp's consent and enquiries endpoints work this way. In plain terms Better to let the visitor through than to block them if something glitches. Why it matters here It means you cannot rely on the response to confirm a durable write — show your success state on the acknowledgement and do not read the row back. Contrast a fail-closed probe like healthz."},{"t":"Feature flag","u":"/glossary/feature-flag/","c":"Developer glossary","e":"Glossary","s":"A feature flag is an owner-gated switch that enables or disables a capability at runtime. When off, an endpoint like the Slice biller list returns empty rather than an error.","b":"Definition A feature flag lets Credicorp turn a surface on or off without a deploy. Some public endpoints are flagged — most visibly the Slice biller list, which returns an empty list when its flag is off. Integrations should treat 'flagged off' as 'unavailable', not as a failure. In plain terms A runtime switch that can hide a capability without changing code. Why it matters here Handle empty or 503-class responses on flagged endpoints gracefully. See the public ring."},{"t":"Feature flag","u":"/glossary/feature-flags/","c":"Developer glossary","e":"Glossary","s":"Feature flag — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is A feature flag is a runtime toggle that enables or disables functionality without a deploy. It lets a platform roll a feature out carefully and switch it off instantly if needed. On the public API Some public endpoints are flag-gated. The Slice billers endpoint is the clearest example: when the flag is off it returns a clean, empty/unavailable response rather than an error. Write integrations to call flag-gated endpoints unconditionally and render gracefully when no data comes back. See Feature flags and availability. Why does a flag-gated endpoint return no data? Because its featur"},{"t":"Feature flags and endpoint availability","u":"/public-api/feature-flags-and-availability/","c":"Public API guides","e":"Public API","s":"Some public endpoints are gated behind feature flags and may return a clean \"unavailable\" response instead of data. The Slice biller registry is the clearest example: when Slice billing is switched off, the endpoint responds with a well-formed empty/unavailable payload rather than an error. Write your integration to call these endpoints unconditionally and render gracefully when nothing comes back.","b":"Why flags exist The platform ships features behind flags so they can be turned on carefully and turned off instantly if needed. A flagged endpoint is always deployed, but its data is only exposed when the flag is on. This is why the Slice billers endpoint can be live yet return no billers — the flag governs availability, not the route. The graceful-off contract A flag-gated public endpoint returns a clean, well-formed response when off — not a 500, not a hang. For billers that means an empty list with a clear signal. Your job is to treat \"no data\" as a normal state: hide the biller picker, ski"},{"t":"Fix consent_required errors","u":"/recipes/fix-consent-required-errors/","c":"Integration recipes","e":"Recipe","s":"consent_required is the most common enquiry rejection. The public enquiry endpoint requires fields.consent to equal the string \"yes\" on every submission. This is a hard gate — there is no bypass — so collect real consent and send it correctly.","b":"The rule Every enquiry submission must include fields.consent equal to the exact string \"yes\". A missing flag, a boolean true, or \"Yes\" all fail with consent_required. Send it right Collect explicit consent in your UI, then send the string.{ \"form\": \"contact-us\", \"fields\": { \"name\": \"Jordan Ellis\", \"email\": \"jordan@example-ltd.co.uk\", \"message\": \"Please call me.\", \"consent\": \"yes\" } } Why it is strict The flag is the record that the person agreed to be contacted. It is deliberately un-bypassable, which is why it is validated as an exact string rather than a loose truthy value. Can I default co"},{"t":"Fixed-window rate limiting","u":"/glossary/fixed-window/","c":"Developer glossary","e":"Glossary","s":"Fixed-window rate limiting — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is A fixed-window rate limiter divides time into equal slots and counts requests within each. When a slot’s count reaches the cap, further requests are rejected until the next slot begins, at which point the counter resets to zero. On the public ring The Credicorp public ring uses a fixed 60-second window with a cap of 60 requests per IP per route. It is simple and predictable: you know the reset boundary from the Retry-After header. The trade-off of any fixed window is a possible burst at the slot boundary; the public ring’s modest cap makes that a non-issue in practice. How is fixed-"},{"t":"GET /.well-known/oauth-authorization-server","u":"/reference/get-well-known-oauth-authorization-server/","c":"API reference","e":"Reference","s":"The OAuth 2.0 authorization-server metadata document (RFC 8414). Advertises the token endpoint, JWKS URI, supported grants and scopes so clients can configure themselves.","b":"Endpoint MethodGETPath/.well-known/oauth-authorization-serverRingpublic (OAuth metadata) Parameters None. A discovery read. Response A JSON metadata document per RFC 8414: the token_endpoint, jwks_uri, introspection_endpoint, supported grant types (client_credentials), scopes and signing algorithms. Point an OAuth library at the base URL and it can read this to configure the whole flow without hard-coded URLs. It is the discovery anchor for client-credentials auth. Errors Cacheable and public. A 429 applies only under extreme load. Using it in practice Most OAuth client libraries can bootstrap"},{"t":"GET /.well-known/oauth-protected-resource","u":"/reference/get-well-known-oauth-protected-resource/","c":"API reference","e":"Reference","s":"The OAuth 2.0 protected-resource metadata document. Tells a client which authorization server guards a resource — used by MCP clients to discover the partner server's auth.","b":"Endpoint MethodGETPath/.well-known/oauth-protected-resourceRingpublic (OAuth metadata) Parameters None. A discovery read. Response A JSON metadata document naming the authorization server(s) that protect a resource and the scopes it expects. MCP clients read it to discover how to authenticate against the partner MCP server: it points at the authorization-server metadata, which in turn points at the token endpoint. The chain lets an OAuth-aware client bootstrap the whole flow from one URL. Errors Cacheable and public. Why do MCP clients need this? An MCP client hitting a token-gated server need"},{"t":"GET /partner/v1/applications/{id}","u":"/reference/get-partner-v1-applications-id/","c":"API reference","e":"Reference","s":"Read a single application's current state on the partner ring: its status, the submitted details and links to the associated decision.","b":"Endpoint MethodGETPath/partner/v1/applications/{id}Ringpartner (OAuth)AuthOAuth Request Path parameter id — the application ID returned when you created it. Requires applications:read. Response A JSON application object: its id, status, the submitted details and a reference to the associated decision where one exists. Use it to reconcile state after a missed webhook or on a support lookup. Errors & notes A 404 if the ID is unknown or not owned by your project. A 403 without applications:read. Reads are cheap but still count against your bucket. When to use it Reach for this read in three situa"},{"t":"GET /partner/v1/decisions/{id}","u":"/reference/get-partner-v1-decisions-id/","c":"API reference","e":"Reference","s":"Read a credit decision on the partner ring: the outcome, any conditions and the offer terms. Decisioning is authoritative; money-out remains a governed manual gate.","b":"Endpoint MethodGETPath/partner/v1/decisions/{id}Ringpartner (OAuth)AuthOAuth Request Path parameter id — the decision (or application) ID. Requires decisions:read. Response A JSON decision object: the outcome, any conditions, and where approved, the offer terms. Credicorp's decisioning is authoritative — the API returns the real decision, not an advisory hint. Even on an approval, money-out stays a governed manual gate, so an approved decision is a green light to proceed through the funded journey, not an instant disbursement. Errors & notes A 404 for an unknown decision; a 409 if the decision"},{"t":"GET /partner/v1/oauth/jwks","u":"/reference/get-partner-v1-oauth-jwks/","c":"API reference","e":"Reference","s":"The JSON Web Key Set: the public keys that sign partner access tokens, so you can verify a token's signature, issuer and audience locally without calling introspection.","b":"Endpoint MethodGETPath/partner/v1/oauth/jwksRingpartner (OAuth key set) Parameters None. A public key-set read. Response A JWKS document — the public keys used to sign partner access tokens. Fetch it (and cache by kid) to verify a bearer JWT's signature, issuer and audience in your own resource server. Keys rotate; on an unknown kid, re-fetch the JWKS rather than rejecting the token outright. This is the local-verification counterpart to introspection. Errors Rate-limited on the token plane. Cache the response; do not fetch it per request. How do I handle key rotation? Cache the JWKS keyed by "},{"t":"GET /partner/v1/payments/{id}","u":"/reference/get-partner-v1-payments-id/","c":"API reference","e":"Reference","s":"Read a payment's status on the partner ring: provisioned, pending, settled or failed. Pairs with the payment webhook for push updates.","b":"Endpoint MethodGETPath/partner/v1/payments/{id}Ringpartner (OAuth)AuthOAuth Request Path parameter id. Requires payments:read. Response A JSON payment object with its current status — provisioned, pending, settled or failed — plus amounts and references. For push updates rather than polling, subscribe to the payment webhook, which fires on settlement. Errors & notes A 404 for an unknown payment; a 403 without payments:read. Treat settlement as authoritative from the webhook or this read, not from provisioning success. Reconciling settlement Settlement is the state that matters most here, and i"},{"t":"GET /public/v1/cms/pages/{key}","u":"/reference/get-cms-page/","c":"API reference","e":"API reference","s":"GET /public/v1/cms/pages/{key} reads a published CMS page by its stable key. It returns the sanitised, ready-to-render content block for a marketing or help page, so a headless front end can pull copy from the hub without a login. Only published content is exposed; drafts are never served here.","b":"What it does This endpoint sits on the public /public/v1 ring — unauthenticated, anonymous and open to any caller. There is no API key and no OAuth token on this ring; the trust boundary is enforced by rate limiting, strict input validation and a server-fixed response shape rather than by a credential.The CMS read endpoint serves one published page block identified by a stable {key} in the path. The HTML is sanitised on the server before it leaves the hub, so a consumer can render it directly. Because only published content is exposed, you can safely point a public front end at this endpoint w"},{"t":"GET /public/v1/config/pricing","u":"/reference/get-public-v1-config-pricing/","c":"API reference","e":"Reference","s":"The authoritative pricing configuration the whole estate reads: the single source of truth for published figures, so marketing pages and your integration never disagree.","b":"Endpoint MethodGETPath/public/v1/config/pricingRingpublic (unauthenticated) Parameters None. Returns the current published pricing config. Response A JSON pricing config — the published figures (rates, fees, ranges, bounds) that drive quotes and marketing copy across the Credicorp estate. Because every surface reads this endpoint, the numbers reconcile everywhere; do not hard-code figures in your integration, read them here so a config change flows to you automatically. Errors A 429 on rate-limit; otherwise 200. See the public error table. Should I cache the pricing config? Briefly, yes — it i"},{"t":"GET /public/v1/healthz","u":"/reference/get-public-v1-healthz/","c":"API reference","e":"Reference","s":"The live health probe on the public ring. Returns a small JSON body indicating the hub is serving; used by monitors, load balancers and integration smoke tests.","b":"Endpoint MethodGETPath/public/v1/healthzRingpublic (unauthenticated) Parameters None. This is a parameterless read. Response A 200 with a small JSON object indicating the service is healthy. The body is safe to cache briefly and contains no sensitive data. A non-2xx (or a 503) indicates the hub is not currently serving — for example during a migration-drift guard, when the probe fails closed rather than lying about readiness. Errors A 503 means unhealthy — do not treat it as an application error to retry blindly; it is a signal that the platform is not ready. A 429 means you exceeded the 60 re"},{"t":"GET /public/v1/loyalty/tiers","u":"/reference/get-public-v1-loyalty-tiers/","c":"API reference","e":"Reference","s":"The loyalty tier ladder: the tier vocabulary, thresholds, headline benefits and cooldown — single-sourced so marketing and your integration read identical figures.","b":"Endpoint MethodGETPath/public/v1/loyalty/tiersRingpublic (unauthenticated, cacheable) Parameters None. Returns the tier ladder vocabulary. Response A JSON object describing the loyalty ladder: each tier's name, the threshold to reach it, its headline benefit and any cooldown. It carries vocabulary and figures only — no actor, no PII, no money — so it is safe to cache. The marketing pages read this exact endpoint so the tier config is single-sourced across the estate. Agents can read the same via the MCP tool loyalty_tiers. Errors A 429 on rate-limit; otherwise 200. See the public error table. "},{"t":"GET /public/v1/loyalty/tiers","u":"/reference/get-loyalty-tiers/","c":"API reference","e":"API reference","s":"GET /public/v1/loyalty/tiers returns the loyalty-tier vocabulary — the four tiers Bronze → Silver → Gold → Platinum, each with its qualification thresholds and the headline arrangement-fee discount earned at that tier. It carries no PII, is safe to cache, and lets a marketing or comparison page render the ladder without bundling any server config.","b":"What it does This endpoint sits on the public /public/v1 ring — unauthenticated, anonymous and open to any caller. There is no API key and no OAuth token on this ring; the trust boundary is enforced by rate limiting, strict input validation and a server-fixed response shape rather than by a credential.The endpoint exposes the canonical loyalty ladder so any surface — the marketing site, a partner comparison page, a mobile app — can render the tiers identically. It returns one figure per tier: the arrangement-fee discount percentage a customer earns at that tier, read verbatim from the hub’s lo"},{"t":"GET /public/v1/mcp","u":"/reference/get-public-v1-mcp/","c":"API reference","e":"Reference","s":"The MCP server card: a discovery probe that returns the server's identity and capabilities so MCP clients can configure themselves. Read-only and cacheable.","b":"Endpoint MethodGETPath/public/v1/mcpRingpublic (unauthenticated, cacheable) Parameters None. A discovery read. Response A JSON server card describing the MCP server — its name, protocol version and advertised capabilities — so an MCP client can discover and configure itself before opening a session with POST /public/v1/mcp. The card is read-only and safe to cache. Errors A 429 on rate-limit; otherwise 200. See the public error table. Discovery flow A well-behaved MCP client reads the server card first, learns the protocol version and advertised capabilities, and only then opens a session with "},{"t":"GET /public/v1/products","u":"/reference/get-public-v1-products/","c":"API reference","e":"Reference","s":"The product catalogue: the three Credicorp products — Business Loan, Credicorp Flex, Credicorp Slice — each with its real name, one-line description and enabled flag.","b":"Endpoint MethodGETPath/public/v1/productsRingpublic (unauthenticated) Parameters None for the base list. The response is the published catalogue. Response A JSON list of products. Each entry carries the product's real name, a one-line description and whether it is currently enabled:Business Loan — a short-term business loan for a one-off, small sum; fixed term, repaid on a simple schedule.Credicorp Flex — a revolving business credit facility; draw what you need up to a limit and repay flexibly.Credicorp Slice — pay a supplier or biller now and repay Credicorp over a short schedule.The same fig"},{"t":"GET /public/v1/quote/flex","u":"/reference/get-public-v1-quote-flex/","c":"API reference","e":"Reference","s":"A representative Credicorp Flex preview — the revolving business credit facility. Illustrates cost of drawing against a limit; published, non-binding figures only.","b":"Endpoint MethodGETPath/public/v1/quote/flexRingpublic (unauthenticated) Parameters Query parameters describe a representative draw against a Flex limit (for example a drawn balance and a holding period). Exact parameter names and any bounds come from the pricing config; read them rather than hard-coding. Response A JSON preview for Credicorp Flex, the revolving business credit facility: an illustrative cost for holding a drawn balance over a period. Because Flex accrues on the outstanding balance for the days you hold it, the preview is representative, not a fixed total — the real cost depends"},{"t":"GET /public/v1/quote/onetime","u":"/reference/get-public-v1-quote-onetime/","c":"API reference","e":"Reference","s":"A representative Business Loan quote for an amount and term. Bounded to £50–£500 principal and 14–84 days by the engine's RangeGuard; returns a published, illustrative figure.","b":"Endpoint MethodGETPath/public/v1/quote/onetimeRingpublic (unauthenticated) Parameters Query parameters:ParamTypeNotesamountinteger (GBP)Principal in whole pounds. Bounded to £50–£500.terminteger (days)Bounded to 14–84 days.frequencyenumweekly | fortnightly | monthly. Response A JSON quote for the Business Loan: the representative repayment breakdown for the requested amount and term. Amounts are labelled with their unit. Only the Business Loan is quotable through this endpoint — it is the always-on product with a pure amount-and-term shape. The same computation is available to agents via the M"},{"t":"GET /public/v1/slice/billers","u":"/reference/get-public-v1-slice-billers/","c":"API reference","e":"Reference","s":"The list of accepted billers for Credicorp Slice — the suppliers and billers a company can pay now and repay over a short schedule. Feature-flagged; may return empty when off.","b":"Endpoint MethodGETPath/public/v1/slice/billersRingpublic (unauthenticated, feature-flagged) Parameters None. Returns the accepted-biller registry. Response A JSON list of accepted billers for Credicorp Slice — the suppliers and billers you can pay immediately and repay Credicorp over a short schedule. This surface is feature-flagged (owner-gated); when the flag is off the endpoint may return an empty list rather than an error, so treat empty as \"none currently available\", not a failure. The marketing accepted-billers page reads exactly this endpoint. Errors A 429 on rate-limit. When the featur"},{"t":"GET /public/v1/slice/billers","u":"/reference/get-slice-billers/","c":"API reference","e":"API reference","s":"GET /public/v1/slice/billers lists the accepted billers for Credicorp Slice — the public-safe view of each live biller: name, category, and the minimum and maximum you can spread across it. Bank and settlement fields are structurally absent from the projection. The registry is feature-flag gated, so when Slice billing is off the endpoint returns a clean unavailable response rather than an error.","b":"What it does This endpoint sits on the public /public/v1 ring — unauthenticated, anonymous and open to any caller. There is no API key and no OAuth token on this ring; the trust boundary is enforced by rate limiting, strict input validation and a server-fixed response shape rather than by a credential.Slice lets a customer spread a supplier or tax bill. This endpoint publishes the list of billers the platform can pay, in a projection that carries only public-safe fields — the biller’s name, its category, and the minimum and maximum amount that can be spread across it. Sensitive routing data (b"},{"t":"GET /public/v1/support/widget.css","u":"/reference/get-support-widget-css/","c":"API reference","e":"API reference","s":"GET /public/v1/support/widget.css serves the support-widget stylesheet — the styles that pair with widget.js to render the Credicorp chat widget. It is a static asset served with a CSS content type and is safe to cache aggressively.","b":"What it does The stylesheet endpoint returns the widget’s CSS. Load it in the document head alongside the script. Keeping the CSS separate lets the browser cache it independently and lets you preload it early. Embed &lt;link rel=\"stylesheet\" href=\"https://hub.credicorp.co.uk/public/v1/support/widget.css\"&gt; Caching Like the script, this is a static asset. Cache it at the CDN and browser; it changes only when the widget’s look changes. Do I need both the CSS and the JS? Yes — the script renders the widget and the stylesheet makes it look right. Load both: the CSS in the head, the script with `"},{"t":"GET /public/v1/support/widget.js","u":"/reference/get-support-widget-js/","c":"API reference","e":"API reference","s":"GET /public/v1/support/widget.js serves the support-widget script — a self-contained JavaScript file you add to any page to render the Credicorp chat widget. It has no build step and no dependencies; it mounts itself and talks to POST /public/v1/support/chat for you.","b":"What it does This endpoint returns the widget’s JavaScript with a JavaScript content type. Include it with a single &lt;script&gt; tag and pair it with the stylesheet; the script mounts the chat UI and wires it to the chat endpoint. Embed &lt;link rel=\"stylesheet\" href=\"https://hub.credicorp.co.uk/public/v1/support/widget.css\"&gt; &lt;script src=\"https://hub.credicorp.co.uk/public/v1/support/widget.js\" defer&gt;&lt;/script&gt; Caching The script is a static asset — cache it aggressively at the CDN and in the browser. It changes only when the widget itself is updated. Does the widget need a bun"},{"t":"HMAC signature","u":"/glossary/hmac/","c":"Developer glossary","e":"Glossary","s":"An HMAC signature is a keyed hash of a message that proves it came from a party holding the shared secret and was not altered. Credicorp signs webhooks and internal hops with it.","b":"Definition HMAC combines a message with a secret key through a hash function to produce a signature only a holder of the secret could compute. The receiver recomputes it over the raw bytes and compares — in constant time — to authenticate the message. In plain terms A tamper-proof stamp that proves who sent a message, using a shared secret. Why it matters here It secures webhook delivery. See verifying a webhook signature."},{"t":"HTML sanitisation","u":"/glossary/sanitisation/","c":"Developer glossary","e":"Glossary","s":"HTML sanitisation — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is Sanitisation removes potentially unsafe markup — scripts, event handlers, disallowed tags — from HTML so it can be rendered without introducing an injection risk. In the Credicorp API The CMS page endpoint returns HTML that is sanitised on the server before it leaves the hub, so a consumer can render it. As defence in depth, still apply your own framework’s escaping and content-security rules at render time. See headless rendering. Is the CMS HTML safe to inject directly? It is sanitised server-side, which removes disallowed markup, but apply your own render-time escaping as belt-an"},{"t":"HTTP 400 Bad Request","u":"/reference/http-400-bad-request/","c":"API reference","e":"API reference","s":"A 400 Bad Request means the request could not be parsed or was structurally wrong. The response body is the standard <a href=\"/reference/error-envelope-and-status-codes/\">error envelope</a> with type: invalid_request_error. This is not retry-able — fix the request or credential first.","b":"What it means The request could not be parsed or was structurally wrong. Common causes Malformed JSON, a wrong or missing Content-Type, or a body that is not the shape the endpoint expects. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"invalid_request\", \"message\": \"The request could not be parsed or was structurally wrong.\" } } How to handle Fix the request before resending — the same bytes will always fail.See the error code catalogue for the specific code that accompanied this status. Where it sits in the error model A 400 is one of the invalid_request_error class o"},{"t":"HTTP 401 Unauthorized","u":"/reference/http-401-unauthorized/","c":"API reference","e":"API reference","s":"A 401 Unauthorized means the request lacked a valid credential (partner plane). The response body is the standard <a href=\"/reference/error-envelope-and-status-codes/\">error envelope</a> with type: authentication_error. This is not retry-able — fix the request or credential first.","b":"What it means The request lacked a valid credential (partner plane). Common causes A missing, expired or malformed API key or OAuth token on a /partner/v1 call. Example response { \"error\": { \"type\": \"authentication_error\", \"code\": \"authentication\", \"message\": \"The request lacked a valid credential (partner plane).\" } } How to handle Re-check the credential. This is not retry-able until auth is fixed.See the error code catalogue for the specific code that accompanied this status. Where it sits in the error model A 401 is one of the authentication_error class of responses. On the Credicorp API t"},{"t":"HTTP 403 Forbidden","u":"/reference/http-403-forbidden/","c":"API reference","e":"API reference","s":"A 403 Forbidden means the caller is authenticated but not allowed to do this. The response body is the standard <a href=\"/reference/error-envelope-and-status-codes/\">error envelope</a> with type: authentication_error. This is not retry-able — fix the request or credential first.","b":"What it means The caller is authenticated but not allowed to do this. Common causes A valid token whose scope does not cover the action, or a resource owned by another account. Example response { \"error\": { \"type\": \"authentication_error\", \"code\": \"authentication\", \"message\": \"The caller is authenticated but not allowed to do this.\" } } How to handle Request the right scope or access; retrying with the same token will not help.See the error code catalogue for the specific code that accompanied this status. Where it sits in the error model A 403 is one of the authentication_error class of respon"},{"t":"HTTP 404 Not Found","u":"/reference/http-404-not-found/","c":"API reference","e":"API reference","s":"A 404 Not Found means the route or resource does not exist. The response body is the standard <a href=\"/reference/error-envelope-and-status-codes/\">error envelope</a> with type: invalid_request_error. This is not retry-able — fix the request or credential first.","b":"What it means The route or resource does not exist. Common causes A typo in the path, a resource id that never existed or has been deleted, or a wrong environment. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"invalid_request\", \"message\": \"The route or resource does not exist.\" } } How to handle Verify the path and id. Do not retry.See the error code catalogue for the specific code that accompanied this status. Where it sits in the error model A 404 is one of the invalid_request_error class of responses. On the Credicorp API the HTTP status is only the broad classific"},{"t":"HTTP 409 Conflict","u":"/reference/http-409-conflict/","c":"API reference","e":"API reference","s":"A 409 Conflict means the request conflicts with the current state. The response body is the standard <a href=\"/reference/error-envelope-and-status-codes/\">error envelope</a> with type: invalid_request_error. This is not retry-able — fix the request or credential first.","b":"What it means The request conflicts with the current state. Common causes Reusing an idempotency key with a different body, or acting on a resource in an incompatible state. Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"invalid_request\", \"message\": \"The request conflicts with the current state.\" } } How to handle Resolve the conflict — send a fresh key or refetch state — then act.See the error code catalogue for the specific code that accompanied this status. Where it sits in the error model A 409 is one of the invalid_request_error class of responses. On the Credicor"},{"t":"HTTP 422 Unprocessable Entity","u":"/reference/http-422-unprocessable-entity/","c":"API reference","e":"API reference","s":"A 422 Unprocessable Entity means the request parsed but a value failed validation. The response body is the standard <a href=\"/reference/error-envelope-and-status-codes/\">error envelope</a> with type: invalid_request_error. This is not retry-able — fix the request or credential first.","b":"What it means The request parsed but a value failed validation. Common causes A missing required field, a bad email, an over-limit payload, or the consent gate not set to \"yes\". Example response { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"validation_failed\", \"message\": \"The request parsed but a value failed validation.\" } } How to handle Read error.param to find the offending field and correct it.See the error code catalogue for the specific code that accompanied this status. Where it sits in the error model A 422 is one of the invalid_request_error class of responses. On the Credic"},{"t":"HTTP 429 Too Many Requests","u":"/reference/http-429-too-many-requests/","c":"API reference","e":"API reference","s":"A 429 Too Many Requests means you exceeded the rate limit. The response body is the standard <a href=\"/reference/error-envelope-and-status-codes/\">error envelope</a> with type: rate_limit_error. This is retry-able with backoff.","b":"What it means You exceeded the rate limit. Common causes More than 60 requests in 60 seconds from one IP on the public ring. Example response { \"error\": { \"type\": \"rate_limit_error\", \"code\": \"rate_limited\", \"message\": \"You exceeded the rate limit.\" } } How to handle Back off until the Retry-After seconds have passed, then retry.Honour the Retry-After header and see Retrying failed requests. Where it sits in the error model A 429 is one of the rate_limit_error class of responses. On the Credicorp API the HTTP status is only the broad classification; the exact reason travels in error.code, and —"},{"t":"HTTP 500 Internal Server Error","u":"/reference/http-500-internal-server-error/","c":"API reference","e":"API reference","s":"A 500 Internal Server Error means an unexpected error occurred on our side. The response body is the standard <a href=\"/reference/error-envelope-and-status-codes/\">error envelope</a> with type: api_error. This is retry-able with backoff.","b":"What it means An unexpected error occurred on our side. Common causes A transient fault in the platform. It is not caused by your request being wrong. Example response { \"error\": { \"type\": \"api_error\", \"code\": \"api\", \"message\": \"An unexpected error occurred on our side.\" } } How to handle Retry with exponential backoff. If it persists, contact support with the request id.Honour the Retry-After header and see Retrying failed requests. Where it sits in the error model A 500 is one of the api_error class of responses. On the Credicorp API the HTTP status is only the broad classification; the exac"},{"t":"HTTP 503 Service Unavailable","u":"/reference/http-503-service-unavailable/","c":"API reference","e":"API reference","s":"A 503 Service Unavailable means the service is temporarily unavailable. The response body is the standard <a href=\"/reference/error-envelope-and-status-codes/\">error envelope</a> with type: api_error. This is retry-able with backoff.","b":"What it means The service is temporarily unavailable. Common causes A brief maintenance window or an overloaded dependency. Example response { \"error\": { \"type\": \"api_error\", \"code\": \"api\", \"message\": \"The service is temporarily unavailable.\" } } How to handle Retry with backoff; the service returns shortly.Honour the Retry-After header and see Retrying failed requests. Where it sits in the error model A 503 is one of the api_error class of responses. On the Credicorp API the HTTP status is only the broad classification; the exact reason travels in error.code, and — for field-level problems — "},{"t":"HTTP status code","u":"/glossary/http-status-codes/","c":"Developer glossary","e":"Glossary","s":"HTTP status code — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is An HTTP status code is the three-digit number that summarises the outcome of a request. 2xx means success, 4xx means the client got something wrong, 5xx means the server failed. The codes you will see The Credicorp public API uses a small, standard set: 200 (read OK), 201 (enquiry created), 400 (malformed request), 404 (unknown/unpublished resource), 422 (validation failed), and 429 (rate limited). Each non-2xx also carries a machine-readable error code. See Errors and status codes. Which status means my enquiry saved? A 201 Created, returning the enquiry id and status \"new\"."},{"t":"Handle a flag-gated empty state cleanly","u":"/recipes/handle-the-flag-gated-empty-state/","c":"Integration recipes","e":"Recipe","s":"Make your UI correct whether a feature is on or off. Flag-gated endpoints like the Slice biller registry return a clean empty state when disabled. Branch on the presence of data — not the HTTP status — so a flag flip on our side needs zero change on yours, and users never see a broken section.","b":"Call unconditionally Always call the billers endpoint; do not guess whether the feature is on. When off, it returns a well-formed empty payload, not an error. Render on presence of data Show the biller picker only when billers is non-empty; otherwise hide the section entirely. This keeps the page correct in both states. Why this matters Because we can flip the flag without warning, an integration that assumes data will always be present breaks the moment the feature is toggled. Branching on presence makes you immune. See Feature flags. Why branch on data, not status The subtle mistake teams ma"},{"t":"Handle out-of-order webhooks","u":"/recipes/handle-out-of-order-webhooks/","c":"Integration recipes","e":"Recipe","s":"Webhook delivery is unordered. A retried payment.failed can land after the payment.succeeded that superseded it. Never drive state purely from arrival order. Compare the event created timestamp against what you have, and re-fetch current state when in doubt.","b":"Guard with timestamps Store the created of the last event you applied per resource. Ignore any event older than that.if (evt.created Can I ask for ordered delivery? No. Order is not guaranteed and cannot be enabled. Reconcile with timestamps and current state instead. Which timestamp do I trust? The envelope `created`, which marks when the event occurred — not when it was delivered. Delivery time drifts with retries; `created` does not. Does ordering matter for every event? It matters wherever two events touch the same field or resource close together — a `payment.failed` then a `payment.succe"},{"t":"Handle payment.failed webhooks","u":"/recipes/handle-payment-failed-webhooks/","c":"Integration recipes","e":"Recipe","s":"A payment.failed webhook needs a humane, correct response. Read the failure_code to understand why, trigger a supportive outreach rather than a punitive one, and reconcile against the live loan state — never assume the payment stayed failed.","b":"Read the reason The payment.failed payload carries a failure_code such as insufficient_funds. Branch your response on it. Respond supportively An insufficient-funds failure is a moment for hardship-aware signposting, not pressure. Trigger a gentle reminder and surface support options rather than an aggressive dunning sequence. Reconcile against live state Because webhooks are unordered, a later payment.succeeded (a retry that cleared) may already have superseded the failure. Re-read the loan before acting on money or status. Should I immediately chase a failed payment? No. Read the `failure_co"},{"t":"Handle rate limits and back off correctly","u":"/recipes/handle-rate-limits-and-backoff/","c":"Integration recipes","e":"Recipe","s":"Do not guess your remaining budget — read the RateLimit- headers, slow down before the wall, and on a 429 wait for Reset with exponential back-off plus jitter.","b":"Read the headers Every response — success or 429 — carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. When Remaining is low, pace yourself before you hit the wall rather than waiting for the 429. This works on both the public 60/60s ring and the partner token bucket. Back off with jitter let delay = base; for (let attempt = 0; attempt &lt; maxAttempts; attempt++) { const res = await call(); if (res.status !== 429 &amp;&amp; res.status &lt; 500) return res; const reset = Number(res.headers.get('RateLimit-Reset')) || delay; await sleep(reset * 1000 + Math.random() * 250); // jitte"},{"t":"Handle rate limits gracefully","u":"/recipes/recover-from-rate-limits/","c":"Integration recipes","e":"Recipe","s":"The public ring allows 60 requests per 60 seconds per IP. Cross it and you get a 429 with Retry-After. Avoid it with client-side throttling and caching; recover from it by honouring Retry-After and backing off with jitter.","b":"Avoid the limit Cache responses that rarely change (product data, loyalty tiers), coalesce duplicate in-flight requests, and throttle client-side so you never burst past the window. See Rate limits and 429. Recover from a 429 On a 429, read Retry-After (seconds) and wait at least that long before retrying. Add jitter so many clients do not retry in lockstep.if (res.status === 429) { const wait = (Number(res.headers.get('Retry-After')) || 1) + Math.random(); await sleep(wait * 1000); return retry(); } Design for it Rate limiting is defence-in-depth, not a reliability gate. Treat a 429 as a norm"},{"t":"Handle rate limits gracefully","u":"/recipes/handle-public-api-rate-limits/","c":"Integration recipes","e":"Recipe","s":"Handle the public ring’s 60/60 s limit without drama. On a 429, read Retry-After, wait that long, and retry once. Add jitter if many clients might retry together, cap exponential back-off for other transient errors, and keep steady-state traffic well under the window so you rarely see a 429 at all.","b":"The rule for 429 A 429 carries a Retry-After header in seconds. Sleep for exactly that long, then retry the request once. Do not retry in a tight loop — that only keeps the window saturated. Add jitter If many clients could hit the limit at once, add a small random jitter to the wait so they do not all retry on the same tick and re-trip the limit. A few hundred milliseconds of spread is plenty. Back off other transient errors For 5xx responses, use exponential back-off capped at a handful of attempts (e.g. 1s, 2s, 4s, then give up). Do not retry 4xx other than 429 — those are deterministic and"},{"t":"Handle the quote range error gracefully","u":"/recipes/handle-the-quote-range-error/","c":"Integration recipes","e":"Recipe","s":"The quote endpoint returns a 422 when amount or term is out of range. Clamp inputs up front, read the bound from the error detail, and never surface the raw error to a user.","b":"Clamp before calling The engine bounds the Business Loan to £50–£500 and 14–84 days. Constrain your inputs — slider min/max, number-field bounds — so a user cannot request a figure that will 422 in the first place. Prevention beats handling. Read the detail on a 422 If a value still slips through (a pasted number, an API caller), the endpoint returns a 422 whose detail states the bound it enforced. Parse that, clamp to the nearest valid value, and either retry automatically or prompt the user — do not show the raw error body. Message like a human Replace the machine error with plain guidance: "},{"t":"Headless-render a CMS page","u":"/recipes/headless-render-a-cms-page/","c":"Integration recipes","e":"Recipe","s":"Pull published Credicorp copy into your own front end from one keyed read. GET /public/v1/cms/pages/{key} returns sanitised, ready-to-render HTML for a published page. Cache it by its updated field, treat a 404 as \"not published\", and you have a headless CMS feed with no login.","b":"Step 1 — read a page by key curl -sS https://hub.credicorp.co.uk/public/v1/cms/pages/responsible-lending Step 2 — render the HTML The html field is sanitised server-side, so you can inject it into your template. Apply your framework’s own escaping rules as defence in depth. Show the title as the heading. Step 3 — cache by updated Cache the response keyed on the page key plus its updated timestamp; refresh when updated moves. See Caching. Handling a missing page An unknown or unpublished key returns 404. Treat it as \"no content here\" — render a fallback, do not error. Drafts are indistinguishab"},{"t":"Idempotency","u":"/glossary/idempotency-explained/","c":"Developer glossary","e":"Glossary","s":"Idempotency — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is Idempotency means that making the same request more than once has the same effect as making it once. Reading a resource is naturally idempotent — fetching the loyalty tiers twice changes nothing. This is why safe retries are fine for the public ring’s GET endpoints. On the public ring Every public GET — loyalty tiers, CMS pages, billers, the MCP server card — is idempotent, so retrying after a network blip or a 429 is safe. The write endpoints (enquiries, consent) are one-shot submissions: only retry them when you are sure the first attempt did not reach the server (for example, a c"},{"t":"Idempotency and safe retries","u":"/public-api/idempotency-and-safe-retries/","c":"Public API guides","e":"Reliability","s":"Send an Idempotency-Key on every mutating partner call. The server records the first result against the key and replays it for any repeat, so a timed-out POST /applications can never open two cases.","b":"Why you need it Networks fail after the server has acted but before you get the response. Without protection, your retry re-submits the application, provisions a second payment link, or runs a duplicate identity check. An idempotency key closes that gap: the first request that carries a given key executes; every later request with the same key returns the stored result of the first, byte for byte, without re-executing. Using a key curl -s -X POST https://hub.credicorp.co.uk/partner/v1/applications \\ -H 'Authorization: Bearer '$TOKEN \\ -H 'Idempotency-Key: 3b1e-app-2026-07-04-0001' \\ -H 'Conten"},{"t":"Idempotency key","u":"/glossary/idempotency-key/","c":"Developer glossary","e":"Glossary","s":"An idempotency key is a header you attach to a mutating request so a retry returns the original result instead of acting twice — submitting one application, not two.","b":"Definition An idempotency key is a unique value you send on a write. The server records the first result against the key and replays it for any repeat, so a network-timeout retry of a POST cannot double-act. Reusing a key with a different body is an error. In plain terms A token that makes a retry safe by guaranteeing the action happens at most once. Why it matters here Essential for reliable writes on the partner ring. See idempotency and safe retries."},{"t":"Indicative quote","u":"/glossary/indicative-quote/","c":"Developer glossary","e":"Glossary","s":"Indicative quote — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is An indicative quote is an illustrative figure showing roughly what borrowing might cost, not a firm, binding offer. A real offer follows a full application and assessment. In the Credicorp API The MCP GetQuote tool returns indicative quotes on the public ring. Present them clearly as examples and never as an offer — a genuine offer only comes from a full application on the authenticated surface. Point interested users to the application or the repayment calculator. Is an MCP quote a real offer? No. It is indicative only. A binding offer requires a full application and assessment on "},{"t":"Initialize an MCP client against Credicorp","u":"/recipes/initialize-an-mcp-client/","c":"Integration recipes","e":"Recipe","s":"Point a standards MCP client at /public/v1/mcp: fetch the server card, initialize, tools/list, then register the tools with your agent. No token for the public server.","b":"Discover, then initialize A well-behaved MCP client first reads the server card (a GET) to learn the server's capabilities and protocol version, then opens a session with an initialize call to POST /public/v1/mcp. For the public server there is no auth step; for the partner server the client reads the protected-resource metadata and obtains a token first. List and register tools Call tools/list to enumerate the six public tools with their input schemas, and register them with your agent framework so the model can invoke them. The schemas tell the model exactly what arguments each tool needs — "},{"t":"Input validation","u":"/glossary/validation/","c":"Developer glossary","e":"Glossary","s":"Input validation — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is Input validation is the server’s check that a request meets its constraints before acting on it — correct types, bounded lengths, allowed values, mandatory fields. On an unauthenticated surface it does much of the work a credential would otherwise do. On the public ring The public ring validates strictly. An enquiry’s fields must be a flat object of ≤60 keys and ≤16 KiB, a present email must be valid, and consent must equal \"yes\". A failure returns 422 with a stable error code. Mirror these rules client-side for a better experience, but the server is always the final authority. See "},{"t":"Internal ring","u":"/glossary/internal-ring/","c":"Developer glossary","e":"Glossary","s":"Internal ring — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is The internal ring is the authenticated tier of the Credicorp API, served under /internal/v1. Unlike the public ring, it requires an authenticated actor and carries the sensitive operations — reading a specific customer’s account, offers, notifications, payments and profile changes. Why it matters to you The internal ring is not part of a public integration. It is far more generously rate limited (around 600 requests per 60 seconds per actor) precisely because callers are known and trusted. If your integration needs anything the public ring does not offer, you are looking at the inte"},{"t":"JSON","u":"/glossary/json/","c":"Developer glossary","e":"Glossary","s":"JSON — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is JSON (JavaScript Object Notation) is a lightweight, text-based data format of objects, arrays, strings, numbers and booleans. It is the lingua franca of web APIs. In the Credicorp API Every Credicorp public endpoint speaks JSON: reads return JSON bodies, and the POST endpoints take JSON request bodies with Content-Type: application/json. The MCP endpoint layers JSON-RPC on top. Do I need to send JSON? Yes for the POST endpoints — send a JSON body with the right Content-Type. Reads return JSON you parse on your side."},{"t":"JSON-RPC","u":"/glossary/json-rpc-explained/","c":"Developer glossary","e":"Glossary","s":"JSON-RPC — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is JSON-RPC is a lightweight remote-procedure-call protocol that encodes a call as JSON: a method name, a params object, and an id to correlate the response, which comes back as a result or an error. In the Credicorp MCP server The MCP endpoint accepts JSON-RPC 2.0 over POST. Clients call standard MCP methods — initialize, tools/list, tools/call — and receive results in kind. A plain GET returns the server card instead, for inspection without a JSON-RPC round trip. Do I write JSON-RPC by hand? Usually not. An MCP-compliant client speaks it for you. You only craft it by hand when testin"},{"t":"JSON-RPC 2.0","u":"/glossary/json-rpc/","c":"Developer glossary","e":"Glossary","s":"JSON-RPC 2.0 is a lightweight RPC protocol carried as JSON — a method name, params, and an id in, a result or error out. It is the transport for Credicorp's MCP server.","b":"Definition JSON-RPC 2.0 is a stateless remote-procedure-call convention. A request names a method, passes params, and carries an id; the response returns a result or a structured error (with codes like -32602 for invalid params). In plain terms A simple, standard way to call a function on a server by sending it JSON. Why it matters here It is how you talk to the MCP server at /public/v1/mcp. See calling the MCP server."},{"t":"JWKS (JSON Web Key Set)","u":"/glossary/jwks/","c":"Developer glossary","e":"Glossary","s":"A JWKS is the published set of public keys that sign partner access tokens. Fetch it, cache by key ID, and verify a token's signature without calling the server.","b":"Definition A JSON Web Key Set is a document of public keys, each with a key ID (kid). To verify an access-token JWT locally, select the key by the token's kid and check the signature. Keys rotate, so a fresh kid means re-fetch the set. In plain terms The public keys that let you check a token is genuine, without asking the server. Why it matters here Served at /partner/v1/oauth/jwks. See verifying a token locally."},{"t":"Jitter","u":"/glossary/jitter/","c":"Developer glossary","e":"Glossary","s":"Jitter — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is Jitter is a small random offset added to a retry delay. Without it, many clients that failed at the same moment retry at the same moment, forming a \"thundering herd\" that re-trips the very limit they are waiting on. When to use it Add a few hundred milliseconds of jitter on top of a Retry-After wait or a back-off step whenever many clients might retry together. For a single client it matters little; at scale it is the difference between recovering and oscillating. See handling rate limits. How much jitter is enough? A few hundred milliseconds of random spread is usually plenty to br"},{"t":"Jitter (backoff)","u":"/glossary/backoff-with-jitter/","c":"Developer glossary","e":"Glossary","s":"Jitter is a random offset added to retry timing so that many clients, having failed together, don’t all retry at the same instant and recreate the overload.","b":"Definition Jitter is the small randomisation you add on top of exponential backoff. Without it, a fleet of clients that failed simultaneously retries in perfect lockstep, hammering a recovering service at each interval. A random offset spreads the retries out. Always jitter your API retries and rate-limit recovery. How much jitter? A common approach is \"full jitter\": wait a random time between zero and the current backoff ceiling. Even a small random component prevents synchronised retry storms."},{"t":"KYC / AML check","u":"/glossary/kyc-aml/","c":"Developer glossary","e":"Glossary","s":"A KYC/AML check verifies a subject's identity and screens for money-laundering risk. Credicorp runs one via the partner identity endpoint, calling an external provider.","b":"Definition Know Your Customer (KYC) and Anti-Money-Laundering (AML) checks confirm who a party is and screen them against sanctions and risk lists. The identity-checks endpoint runs one through an external provider, so the definitive result may be asynchronous. In plain terms The identity and anti-fraud checks required before onboarding a customer. Why it matters here Because it calls an external provider, the endpoint is sub-limited and the result can arrive by webhook. See the identity-checks reference."},{"t":"Keep a comparison page fresh with live data","u":"/recipes/localise-a-comparison-with-live-data/","c":"Integration recipes","e":"Recipe","s":"Never let a comparison page drift from the truth. Read products, details and indicative quotes from the MCP server — at build time for a static page, or at request time behind a short cache for a dynamic one — and your comparison always reflects the current Credicorp data. No manual copy to fall out of date.","b":"Static: read at build time Call ListProducts and ProductDetails in your build step and bake the results into the page. Rebuild when you want to refresh. Dynamic: read behind a cache For a page that must always be current, read at request time through a short server-side cache so you stay under the rate limit. See caching. Label quotes as indicative If you include a GetQuote figure, mark it as an example — see indicative quote. Choosing build-time or request-time The right approach depends on how often your comparison must change. A statically built page — reading the products and details durin"},{"t":"Least privilege","u":"/glossary/least-privilege/","c":"Developer glossary","e":"Glossary","s":"Least privilege means granting only the minimum access a job needs. Applied here: scope every credential narrowly and run one project per workload.","b":"Definition Least privilege is the principle that a credential should hold only the permissions its task requires, and no more. In the Credicorp API this means requesting only the scopes you use and provisioning a separate project per integration. In plain terms Give each key only the powers it actually needs, so a leak is contained. Why it matters here A narrowly-scoped, single-purpose credential turns a compromise into a bounded incident. See scopes and least privilege."},{"t":"List Slice billers safely","u":"/recipes/list-slice-billers/","c":"Integration recipes","e":"Recipe","s":"List the billers a customer can spread a bill across — and handle the off state cleanly. GET /public/v1/slice/billers returns public-safe biller fields (name, category, min, max). Because it is feature-flag gated, call it unconditionally and render the picker only when billers come back; an empty list is a normal, non-error state.","b":"Step 1 — call it curl -sS https://hub.credicorp.co.uk/public/v1/slice/billers Step 2 — render public-safe fields Each biller carries only name, category, min, max and terms. Bank and settlement data is structurally absent — there is nothing sensitive to filter out. Step 3 — handle the empty state If Slice billing is flag-gated off, you get a clean unavailable response with no billers. Hide the picker and move on — do not treat it as an error. See Feature flags and availability. Why is the biller list empty? Slice billing is flag-gated off. The endpoint is live but exposes billers only when the"},{"t":"Live mode vs test mode","u":"/glossary/livemode/","c":"Developer glossary","e":"Glossary","s":"livemode is the boolean on every webhook and resource that says whether it is real production data (true) or sandbox test data (false).","b":"Definition Live mode and test mode are fully separate environments. Every webhook envelope carries livemode: true for production, false for sandbox. Test and live have separate data, separate webhook endpoints and separate signing secrets. A well-built handler asserts the mode it expects and rejects the other, so a stray test event never touches production logic. Test everything in sandbox first — see Test a webhook locally. Can a test event reach my live endpoint? Endpoints are registered per mode, so no — but assert `livemode` in your handler anyway as a safety net."},{"t":"Log and observe your API calls","u":"/recipes/log-and-observe-api-calls/","c":"Integration recipes","e":"Recipe","s":"Instrument every call: capture the request ID, record RateLimit headers, redact secrets, and alert on error-rate and latency. Good observability turns a silent regression into an early warning.","b":"Capture the right fields For each call, log the method, path, status, latency and any request/correlation ID the response carries — that ID is what Credicorp support will ask for. Record the RateLimit-Remaining so you can chart how close you run to the wall. Do not log the bearer token or client secret; redact them. Alert on the signals that matter Set alerts on rising 5xx or 429 rates (a sign you are being throttled — revisit back-off), on climbing latency, and on webhook processing lag. A 429 spike often means you have outgrown your tier or lost your caching. Trace across the webhook boundar"},{"t":"MCP Streamable HTTP","u":"/glossary/streamable-http/","c":"Developer glossary","e":"Glossary","s":"MCP Streamable HTTP is the MCP transport Credicorp uses: a client POSTs one JSON-RPC request and receives one response over plain HTTP, with no persistent socket required.","b":"Definition Streamable HTTP is the MCP transport that runs the protocol over ordinary HTTP request/response, optionally streaming. Credicorp's server handles one JSON-RPC request per POST to /public/v1/mcp and returns one response — simple to call from any HTTP client. In plain terms A way to run MCP over normal web requests instead of a special connection. Why it matters here It means you can drive the MCP server with plain curl — see calling the MCP server."},{"t":"MCP methods: initialize, tools/list, tools/call","u":"/reference/mcp-methods/","c":"API reference","e":"API reference","s":"Three JSON-RPC methods drive the Credicorp MCP server. initialize negotiates the protocol revision and returns server identity; tools/list returns the tools and their input schemas; tools/call invokes a named tool with arguments. A compliant client sends them in that order; you can also drive them by hand with curl.","b":"initialize The handshake. Returns protocolVersion (2025-06-18), server name/version (Credicorp v1.0.0) and capabilities.{ \"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{} } tools/list Returns each tool with its full input schema — the authoritative source for arguments.{ \"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\",\"params\":{} } tools/call Invokes a tool by name with an arguments object.{ \"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{ \"name\":\"ListProducts\",\"arguments\":{} } } Or just read the card A plain GET on the endpoint returns the server card — protocol, identity and a to"},{"t":"MCP server card","u":"/glossary/mcp-server-card/","c":"Developer glossary","e":"Glossary","s":"The server card is the MCP discovery document — a GET on the MCP endpoint returns the server's name, protocol version and capabilities so a client can configure itself.","b":"Definition An MCP server card is the read-only descriptor a client fetches with a GET on the MCP endpoint before opening a session. It names the server, its protocol version and its advertised capabilities, letting the client configure itself without hard-coded assumptions. In plain terms A little 'about me' file the MCP server publishes so clients know how to talk to it. Why it matters here Cacheable and public. Read it, then initialize — see initializing an MCP client."},{"t":"MCP server card","u":"/glossary/server-card/","c":"Developer glossary","e":"Glossary","s":"MCP server card — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is A server card is the document an MCP server publishes describing itself — its protocol revision, identity, capabilities and a summary of its tools — so a client knows what it can do before it connects. In the Credicorp API The Credicorp MCP server returns its card on a plain GET: protocolVersion 2025-06-18, server name Credicorp v1.0.0, and a name+description per tool. For full input schemas, issue a tools/list JSON-RPC call. How do I read the server card? Send a plain GET to `/public/v1/mcp`. It returns the protocol revision, server identity and a tool summary without any JSON-RPC."},{"t":"MCP tool","u":"/glossary/mcp-tool/","c":"Developer glossary","e":"Glossary","s":"An MCP tool is a named function with a typed input schema that an agent invokes via tools/call. Credicorp's public server exposes six: products, details, quote, eligibility, how-to-apply, loyalty.","b":"Definition An MCP tool is a callable unit exposed by an MCP server: it has a name, a description, and an inputSchema the agent uses to build valid arguments. The agent lists tools with tools/list and invokes one with tools/call. In plain terms One of the specific things an agent can ask the server to do. Why it matters here Credicorp's public tools are read-only and return authoritative figures — see the catalogue and, for example, get_quote."},{"t":"MCP tool: EligibilityCriteria","u":"/reference/mcp-eligibility-criteria-tool/","c":"API reference","e":"API reference","s":"EligibilityCriteria returns the baseline a business must meet to be considered for Credicorp finance — a read-only MCP tool an agent can use to answer \"can my business apply?\" accurately. It reports the criteria; it does not assess a specific applicant.","b":"What it does EligibilityCriteria returns the standard requirements a UK limited company must meet — the sort of baseline covered in the eligibility answer. It states the rules; it never runs an assessment on a real applicant. Call it { \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\", \"params\": { \"name\": \"EligibilityCriteria\", \"arguments\": {} } } Not a credit decision The tool reports criteria only. A genuine eligibility and affordability assessment happens during a full application on the authenticated surface — never on the public ring. Criteria versus a decision It is worth being precise a"},{"t":"MCP tool: GetQuote","u":"/reference/mcp-get-quote-tool/","c":"API reference","e":"API reference","s":"GetQuote returns an indicative quote for a stated amount and term as a read-only MCP tool. It is an illustration, not an offer — a real offer needs a full application. Use it to show an example figure in a comparison or an agent conversation, always clearly labelled as indicative.","b":"What it does GetQuote takes an amount and term and returns an indicative cost illustration. It cannot and does not make an offer — see indicative quote. Call it { \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\", \"params\": { \"name\": \"GetQuote\", \"arguments\": { \"amount\": 25000, \"term_months\": 12 } } } Label it clearly Always present the result as an example, never as a binding figure. Send anyone who wants to proceed to the application or the repayment calculator for their own numbers. Why indicative matters An indicative figure is genuinely useful — it helps a visitor picture the shape of a fa"},{"t":"MCP tool: HowToApply","u":"/reference/mcp-how-to-apply-tool/","c":"API reference","e":"API reference","s":"HowToApply returns the application steps and what a business should prepare — a read-only MCP tool that lets an agent guide someone toward applying without doing it for them. It explains the path; the application itself happens on the authenticated surface.","b":"What it does HowToApply returns the ordered steps to apply and the documents to prepare — mirroring the how-to-apply guide. It informs; the agent cannot submit the application. Call it { \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\", \"params\": { \"name\": \"HowToApply\", \"arguments\": {} } } Where the application happens The tool ends by pointing to the real application. Applications are authenticated and never occur on the public ring. Guiding without acting The value of HowToApply is that it lets an assistant be genuinely helpful right up to the boundary of action. It can lay out the steps, e"},{"t":"MCP tool: ListProducts","u":"/reference/mcp-list-products-tool/","c":"API reference","e":"API reference","s":"ListProducts returns the Credicorp lending line-up as a read-only MCP tool: Business Loan, Credicorp Flex and Credicorp Slice. It takes no arguments and is the natural entry point for an AI agent building a picture of what Credicorp offers before drilling into ProductDetails.","b":"What it does ListProducts returns the three Credicorp lending products with a short description each. It is the discovery call an agent makes first, then follows up with ProductDetails for specifics or GetQuote for a figure. Call it { \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\", \"params\": { \"name\": \"ListProducts\", \"arguments\": {} } } The products Business Loan — a fixed-term facility for UK limited companies.Credicorp Flex — a flexible line of credit you draw and repay as needed.Credicorp Slice — spread a supplier or tax bill over months. Why start here An agent that calls ListProducts f"},{"t":"MCP tool: LoyaltyTiers","u":"/reference/mcp-loyalty-tiers-tool/","c":"API reference","e":"API reference","s":"LoyaltyTiers is a read-only MCP tool on the Credicorp MCP server that returns the four-tier loyalty ladder — the same vocabulary and per-tier arrangement-fee discount served by GET /public/v1/loyalty/tiers, delivered through the Model Context Protocol so an AI agent can read it as a tool call.","b":"What it does LoyaltyTiers lets an agent connected to the Credicorp MCP server read the loyalty ladder without calling REST directly. It returns the same figures as GET /public/v1/loyalty/tiers: the four tiers in order, their thresholds, and the arrangement-fee discount in percent units. Call it curl -sS -X POST \\ https://hub.credicorp.co.uk/public/v1/mcp \\ -H 'Accept: application/json' \\ -H 'Content-Type: application/json' \\ -d '{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"tools/call\",\"params\":{\"name\":\"LoyaltyTiers\",\"arguments\":{}}}' When to use the tool vs the REST endpoint Use the MCP tool when an AI a"},{"t":"MCP tool: ProductDetails","u":"/reference/mcp-product-details-tool/","c":"API reference","e":"API reference","s":"ProductDetails returns the full detail of one Credicorp product — Business Loan, Credicorp Flex or Credicorp Slice — as a read-only MCP tool. Pass the product name; get its description, how it works and who it suits. Pair it with ListProducts to build a comparison from the source.","b":"What it does ProductDetails takes a product name and returns the specifics an agent needs to describe it accurately. Use it after ListProducts to drill into a chosen product. Call it { \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\", \"params\": { \"name\": \"ProductDetails\", \"arguments\": { \"name\": \"Credicorp Flex\" } } } Building a comparison Call it once per product and render the results side by side — see build a comparison page. Because it reads from the hub, your comparison never drifts from the truth. Why it matters Accurate product detail is where AI answers usually go wrong — a model half"},{"t":"MCP tool: eligibility_criteria","u":"/reference/mcp-tool-eligibility-criteria/","c":"API reference","e":"MCP tool","s":"Returns Credicorp's eligibility criteria: who can apply. The headline is that Credicorp lends to UK limited companies, not to individuals, with no personal guarantee — so an agent","b":"Tool Nameeligibility_criteriaTransportJSON-RPC 2.0 via POST /public/v1/mcp (tools/call)AuthNone (public ring) What it does Returns Credicorp's eligibility criteria: who can apply. The headline is that Credicorp lends to UK limited companies, not to individuals, with no personal guarantee — so an agent can qualify a prospect before pointing them at the application. Input No arguments. Output A structured summary of eligibility — the entity type (UK limited company), the no-personal-guarantee framing, and the broad conditions a company should meet. It pairs naturally with how_to_apply so an agen"},{"t":"MCP tool: get_quote","u":"/reference/mcp-tool-get-quote/","c":"API reference","e":"MCP tool","s":"Computes a representative Business Loan quote for an amount (GBP) and a term (days). Only the Business Loan is quotable — the always-on product with a pure amount-and-term shape. T","b":"Tool Nameget_quoteTransportJSON-RPC 2.0 via POST /public/v1/mcp (tools/call)AuthNone (public ring) What it does Computes a representative Business Loan quote for an amount (GBP) and a term (days). Only the Business Loan is quotable — the always-on product with a pure amount-and-term shape. The engine bounds inputs to £50–£500 and 14–84 days. Input Arguments:ArgTypeNotesproductenumOnly business_loan (default).amountnumberPrincipal in whole GBP; bounded £50–£500.termnumberTerm in days; bounded 14–84.frequencyenumweekly | fortnightly | monthly.amount and term are required. Output A representative"},{"t":"MCP tool: how_to_apply","u":"/reference/mcp-tool-how-to-apply/","c":"API reference","e":"MCP tool","s":"Returns the steps to apply for Credicorp finance, so an agent can walk a qualified company through the journey and hand it off to the hosted application at the right moment.","b":"Tool Namehow_to_applyTransportJSON-RPC 2.0 via POST /public/v1/mcp (tools/call)AuthNone (public ring) What it does Returns the steps to apply for Credicorp finance, so an agent can walk a qualified company through the journey and hand it off to the hosted application at the right moment. Input No arguments. Output An ordered list of application steps — from starting the application to the decision — ending at the hosted apply journey. An agent typically calls eligibility_criteria, then get_quote, then how_to_apply, then hands the user to apply online. Where does the application actually happen"},{"t":"MCP tool: list_products","u":"/reference/mcp-tool-list-products/","c":"API reference","e":"MCP tool","s":"Returns the three Credicorp products — Business Loan, Credicorp Flex and Credicorp Slice — each with its real name, a one-line description and whether it is currently enabled. The","b":"Tool Namelist_productsTransportJSON-RPC 2.0 via POST /public/v1/mcp (tools/call)AuthNone (public ring) What it does Returns the three Credicorp products — Business Loan, Credicorp Flex and Credicorp Slice — each with its real name, a one-line description and whether it is currently enabled. The starting point for any agent building an accurate picture of what Credicorp offers. Input No arguments. Output An array of products, each with name, description and an enabled flag. Names and one-liners are taken verbatim from the hub's authoritative product narrative, so they reconcile with the product"},{"t":"MCP tool: loyalty_tiers","u":"/reference/mcp-tool-loyalty-tiers/","c":"API reference","e":"MCP tool","s":"Returns the Credicorp loyalty ladder — the tiers, their thresholds and headline benefits — the same vocabulary the public loyalty endpoint publishes, so an agent can explain the re","b":"Tool Nameloyalty_tiersTransportJSON-RPC 2.0 via POST /public/v1/mcp (tools/call)AuthNone (public ring) What it does Returns the Credicorp loyalty ladder — the tiers, their thresholds and headline benefits — the same vocabulary the public loyalty endpoint publishes, so an agent can explain the rewards accurately. Input No arguments. Output The tier ladder with each tier's name, threshold, headline benefit and any cooldown — reconciling with GET /public/v1/loyalty/tiers. It carries vocabulary and figures only, no customer data. How agents use it A lending assistant typically calls loyalty_tiers "},{"t":"MCP tool: product_details","u":"/reference/mcp-tool-product-details/","c":"API reference","e":"MCP tool","s":"Returns a single product's published description plus its byte-exact representative figures. The description is the hub-canonical one-liner and the figures come from the authoritat","b":"Tool Nameproduct_detailsTransportJSON-RPC 2.0 via POST /public/v1/mcp (tools/call)AuthNone (public ring) What it does Returns a single product's published description plus its byte-exact representative figures. The description is the hub-canonical one-liner and the figures come from the authoritative pricing config, so an agent quoting them is quoting the real numbers. Input One argument:productOne of business_loan, flex, slice. Output The product's name, canonical description and representative figures. For the Business Loan this includes the quotable range; for Flex, the revolving-facility f"},{"t":"Make a webhook handler idempotent","u":"/recipes/make-a-webhook-handler-idempotent/","c":"Integration recipes","e":"Recipe","s":"At-least-once delivery means the same event can arrive twice — after a slow acknowledgement or a retry. Deduplicate on the event id: record processed ids, and short-circuit any you have seen. This recipe shows the insert-first pattern that is race-safe.","b":"The pattern Use a unique constraint on the event id and let the database reject the duplicate for you, rather than a read-then-write that races under concurrent retries.async function handle(evt) { try { await db.insert('processed_events', { id: evt.id }); // unique PK } catch (e) { if (isDuplicateKey(e)) return; // already handled — no-op throw e; } await applyEffect(evt); // your business logic } Why not read-then-write Two concurrent retries can both read \"not seen\" before either writes, and both process the event. The unique-insert pattern makes the database the arbiter, so exactly one win"},{"t":"Make your first public-API call","u":"/recipes/make-your-first-public-api-call/","c":"Integration recipes","e":"Recipe","s":"Your first call to the Credicorp public API takes about a minute and needs no credentials. Read the loyalty ladder with a single GET, inspect the JSON, and you have proven the ring works. This recipe walks the whole loop: the request, the response, and what to do if you hit the rate limit.","b":"Step 1 — call a read endpoint The loyalty-tiers endpoint is the friendliest first call: a plain GET, no body, no auth.curl -sS https://hub.credicorp.co.uk/public/v1/loyalty/tiers Step 2 — read the response You get a 200 with the four tiers in Bronze→Platinum order, each carrying its thresholds and arrangement-fee discount (in percent units), plus a generated timestamp. See the full reference for the field-by-field shape. Step 3 — handle the rate limit The public ring allows 60 requests per 60 seconds per IP. If you loop this call in a tight script you will eventually get a 429 with a Retry-Aft"},{"t":"Map errors to user-facing messages","u":"/recipes/map-errors-to-user-messages/","c":"Integration recipes","e":"Recipe","s":"Users should never see a raw API error. Branch on the stable error.code, keep the developer-facing message in your logs, and show a clear, human sentence — pointing at the exact field when param is present.","b":"Switch on code Maintain a small map from code to user copy.const COPY = { consent_required: 'Please tick the consent box to continue.', invalid_email: 'That email address doesn’t look right.', rate_limited: 'You’re going a bit fast — try again in a moment.', }; showUser(COPY[code] || 'Something went wrong. Please try again.'); Keep raw detail internal Log status, code, param and message for yourself; the user gets your curated copy. The envelope is for you, not them. Guide the fix When param is present, highlight that field in your form so the user knows exactly what to correct. Why not surfac"},{"t":"Migrate from scraping to the API","u":"/recipes/migrate-from-scraping-to-the-api/","c":"Integration recipes","e":"Recipe","s":"If you scrape Credicorp figures today, map each value to an endpoint and cut over. Products, pricing, quotes and loyalty are all published — the API is accurate, stable and won't break on a layout change.","b":"Map scraped values to endpoints Everything a scraper pulls from the marketing site is published on the public ring: product names and descriptions from products, figures from config/pricing, live quotes from quote/onetime, and the ladder from loyalty/tiers. Build a mapping table from each scraped field to its endpoint. Cut over and delete the scraper Because these endpoints are the single source the site itself reads, the API values reconcile exactly with what you were scraping — so cut-over is low risk. Replace each scrape with its API read, verify parity, then delete the scraper. You lose th"},{"t":"Mirror partner state into your systems with webhooks","u":"/recipes/mirror-state-with-webhooks/","c":"Integration recipes","e":"Recipe","s":"Mirror Credicorp state by consuming webhooks idempotently and reconciling with reads after any outage. Webhooks push the deltas; reads repair the gaps.","b":"Consume the events Subscribe to the application, decision and payment events. On each, verify the signature, then upsert your local record keyed on the resource ID — idempotently, ordering by state not arrival. This keeps your mirror current in near real time. Reconcile after a gap Webhooks are at-least-once, but if your endpoint was down past the retry window you may miss one. Run a periodic reconciliation: read the resources you care about via their read endpoints (paginating with cursors) and repair any drift. Webhooks are the fast path; reads are the safety net. Model the money-out state M"},{"t":"Mirror the loyalty ladder in a mobile app","u":"/recipes/mirror-the-loyalty-ladder-in-an-app/","c":"Integration recipes","e":"Recipe","s":"Show the loyalty ladder in a native or mobile app from one cached fetch. Pull the tiers on launch, cache them locally keyed on the generated timestamp, and render each tier’s percent-unit discount. Refresh in the background so the ladder is always ready and never a blocking call.","b":"Fetch on launch Call GET /public/v1/loyalty/tiers when the app starts. It is a small, keyless, cacheable read. Cache locally Store the response with its generated timestamp. On next launch, show the cached ladder immediately and refresh in the background — see caching the ladder. Render correctly Show the tiers in the order received (Bronze→Platinum) and the discount as a percent directly (0.5 → \"0.5%\"). Getting the app experience right In a native or mobile app the network is the enemy of a smooth first impression, so the pattern here matters. Fetching the ladder once and caching it locally m"},{"t":"Mirror webhook events into a queue","u":"/recipes/mirror-webhook-events-into-a-queue/","c":"Integration recipes","e":"Recipe","s":"Acknowledge fast by enqueuing, not processing. Verify the signature, push the raw event onto your own queue, and return 200 in milliseconds. Your workers then process at their own pace with retry logic you control — and you never trip the 10-second delivery timeout.","b":"Receive and enqueue The webhook handler does the minimum and returns immediately.app.post('/webhooks', express.raw({type:'*/*'}), async (req, res) => { if (!verify(req.body, req.header('Credicorp-Signature'), SECRET)) return res.status(400).end(); await queue.push(req.body.toString()); // raw event res.status(200).end(); }); Process from the queue Workers parse, dedupe on the event id, and apply effects. If a worker fails, your queue retries — separate from Credicorp’s delivery retries. Why this shape It cleanly separates receipt (must be fast and reliable) from processing (can be slow, can fa"},{"t":"Model Context Protocol (MCP)","u":"/glossary/model-context-protocol/","c":"Developer glossary","e":"Glossary","s":"Model Context Protocol (MCP) is an open standard for exposing tools to language-model agents over JSON-RPC. Credicorp runs a public MCP server so agents can fetch live figures directly.","b":"Definition MCP is a standard that lets an AI agent discover and call tools — functions with typed inputs and outputs — over JSON-RPC 2.0. Credicorp's public MCP server exposes six read-only tools (products, quotes, eligibility, how-to-apply, loyalty) at /public/v1/mcp. In plain terms The plug that lets an assistant call Credicorp for real numbers instead of guessing. Why it matters here It means an agent can quote accurate Credicorp figures with no scraping and no hallucination. See the MCP server overview and the tool catalogue."},{"t":"Model Context Protocol (MCP)","u":"/glossary/mcp-model-context-protocol/","c":"Developer glossary","e":"Glossary","s":"Model Context Protocol (MCP) — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is The Model Context Protocol (MCP) is an open standard that lets AI agents discover and call tools in a uniform way. A server advertises its capabilities and tools; a compliant client initialises, lists the tools, and calls them as a conversation requires. The Credicorp MCP server Credicorp runs an MCP server at /public/v1/mcp, identifying as Credicorp v1.0.0 on protocol revision 2025-06-18. It exposes six strictly read-only tools — ListProducts, ProductDetails, GetQuote, EligibilityCriteria, HowToApply and LoyaltyTiers — so an agent can answer product questions accurately from the so"},{"t":"Money-out gate","u":"/glossary/money-out-gate/","c":"Developer glossary","e":"Glossary","s":"The money-out gate is the one governed manual control in the platform: releasing funds. Everything else, decisioning included, is automated.","b":"Definition Money-out is the release of funds out of Credicorp. It is the platform's single hard, manual gate: an approved decision and a provisioned payment are automated, but the actual disbursement passes through a governed control rather than firing automatically. In plain terms The one step where a human genuinely signs off: money leaving Credicorp. Why it matters here Design your integration to show 'approved, funding in progress' until the settlement event confirms. See money-out: the one gate."},{"t":"Money-out: the one manual gate","u":"/public-api/money-out-is-the-only-gate/","c":"Public API guides","e":"Platform","s":"Everything in the platform is automated except money-out, which is a single governed manual gate. Decisioning is authoritative; the one hard control is releasing funds.","b":"What the gate is Credicorp automates the full journey — quoting, application, authoritative decisioning, payment collection — with exactly one deliberate manual control: the release of funds out of Credicorp (money-out). An approved decision is a real approval, but the disbursement itself passes through a governed gate rather than firing automatically. What it means for your integration Design for it. An approval is a green light to progress the customer through the funded journey, not a signal that money has already moved. Track the funded state via the payment status and the settlement webho"},{"t":"Monitor public-API health from your side","u":"/recipes/monitor-the-public-api-health/","c":"Integration recipes","e":"Recipe","s":"Watch the public API from your side with a light touch. A periodic GET on a cacheable endpoint like the loyalty tiers is a fine liveness probe — just keep the interval well under the rate limit and treat a 200 with a fresh generated timestamp as healthy. Never health-check by hammering a write endpoint.","b":"Pick a read probe Use a cheap, cacheable read — the loyalty tiers endpoint is ideal. A single request every minute or two is plenty and stays far under the 60/60 s window. Define healthy Healthy is a 200 with a well-formed body and a recent generated timestamp. A 429 means you are probing too hard, not that the API is down — back off. A 5xx or a timeout is a real signal. Do not probe writes Never health-check by posting enquiries or consent — that pollutes real data and burns your rate budget. Probe reads only. Which endpoint should I use for uptime checks? A cacheable read like `GET /public/v"},{"t":"Monitor webhook endpoint health","u":"/recipes/monitor-webhook-endpoint-health/","c":"Integration recipes","e":"Recipe","s":"A silently failing endpoint loses events, then gets disabled after 7 days. Monitor delivery success from your side, alert on a run of failures, and know the auto-disable threshold so you fix problems inside the retry window.","b":"Track from your side Log every received event id and its outcome. A sudden gap in ids or a spike in handler errors is your earliest signal. Alert on failure runs Alert if your handler returns non-2xx repeatedly, or if you stop receiving expected event types. Fix inside the 72-hour retry window and no events are lost. Know the disable threshold Seven consecutive days of all-failed deliveries disables the endpoint. Re-enable it once healthy; events still inside their retention are delivered. How will I know if my endpoint was disabled? The partner dashboard flags a disabled endpoint. Monitoring "},{"t":"Move from the public API to the partner API","u":"/recipes/move-from-public-to-partner-api/","c":"Integration recipes","e":"Recipe","s":"When you need accounts, offers or payments, you have outgrown the public ring. The public API is for reads and safe submissions; anything per-customer or money-moving lives behind authentication. This is the signpost: what stays public, what triggers the move, and where the partner API docs live.","b":"What stays on the public ring Product data, the loyalty ladder, published pages, the support widget, the MCP server, enquiry submission and cookie consent — all public, all keyless. If your integration only ever touches these, you never leave the public ring. What triggers the move The moment you need to read a specific customer’s account, create or accept an offer, or move money, you are past the public ring. Those operations require an authenticated actor and, for partners, OAuth2 and request signing. Where to go The partner API reference and its authentication docs (OAuth2, request signing)"},{"t":"OAuth 2.0 client credentials for partner/v1","u":"/public-api/oauth2-client-credentials/","c":"Public API guides","e":"OAuth 2.0","s":"Partner API calls authenticate with the OAuth 2.0 client-credentials grant. You exchange a client ID and secret for a short-lived bearer JWT, scope it to what you use, and send it as Authorization: Bearer.","b":"The grant, step by step Client credentials is the machine-to-machine OAuth grant: there is no user in the loop, so no browser redirect and no authorisation code. Your server holds a client_id and client_secret, posts them to the token endpoint, and receives an access token it uses until the token expires — then it repeats. Keep the secret on the server only; it must never reach a browser or mobile client.Mint a token at POST /partner/v1/oauth/token: Requesting a token curl -s -X POST https://hub.credicorp.co.uk/partner/v1/oauth/token \\ -H 'Content-Type: application/x-www-form-urlencoded' \\ -d "},{"t":"OAuth discovery document","u":"/glossary/discovery-document/","c":"Developer glossary","e":"Glossary","s":"A discovery document is the /.well-known metadata that advertises an OAuth server's endpoints, so a client configures itself from one URL instead of hard-coded links.","b":"Definition Credicorp publishes two: /.well-known/oauth-authorization-server (RFC 8414) describes the token endpoint, JWKS URI and supported grants; /.well-known/oauth-protected-resource tells a client which authorization server guards a resource — used by MCP clients. In plain terms A standard file that lets OAuth clients wire themselves up automatically. Why it matters here Point your library at the base URL and it reads these. See the authorization-server and protected-resource docs."},{"t":"OAuth2","u":"/glossary/oauth2/","c":"Developer glossary","e":"Glossary","s":"OAuth2 — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is OAuth2 is an authorization framework that lets an application obtain a scoped, time-limited access token to act on a resource owner’s behalf, without handling their credentials directly. Where it applies at Credicorp OAuth2 is not used on the public ring — that surface is keyless by design. It comes into play on the partner API, where account-level and money-moving operations require an authenticated, scoped actor. If your integration needs those, see the partner OAuth2 docs and moving from the public API. Do I need OAuth2 for the public ring? No. The public ring uses no credentials"},{"t":"PECR","u":"/glossary/pecr-cookie-consent/","c":"Developer glossary","e":"Glossary","s":"PECR — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is PECR — the Privacy and Electronic Communications Regulations — is the UK law governing cookies and electronic marketing. Under PECR, non-essential cookies (analytics, marketing) require the visitor’s prior consent, and you must be able to evidence it. At Credicorp The consent endpoint records a PECR cookie-consent snapshot into an append-only audit trail, so the estate can always evidence what a visitor agreed to and when. This is cookie consent under PECR §6 / GDPR recital 47 — distinct from credit-application consent, which is stored separately. See Privacy and PII. Is PECR consen"},{"t":"PECR consent","u":"/glossary/pecr/","c":"Developer glossary","e":"Glossary","s":"PECR consent is the UK cookie-consent (analytics/marketing booleans) recorded by the public consent endpoint. It is separate from any consent given inside a credit application.","b":"Definition The Privacy and Electronic Communications Regulations govern cookies and electronic marketing. A visitor's cookie-banner choices are PECR/GDPR consent, recorded via POST /public/v1/consent — not to be confused with the credit-application consent captured on an authenticated path. In plain terms The cookie-banner yes/no, logged so there is a record of what the visitor agreed to. Why it matters here The endpoint is fail-open so the banner UX never blocks on the hub. See capturing a lead."},{"t":"PII (personal data)","u":"/glossary/pii/","c":"Developer glossary","e":"Glossary","s":"PII (personal data) — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is PII, or personal data, is any information that identifies a specific individual — a name, email, account number, or a combination that singles someone out. Handling it carries GDPR duties. On the public ring The public ring enforces a hard rule: no per-customer PII crosses it. Responses are public vocabulary or public-safe projections; the loyalty endpoint returns tier definitions, not a person’s tier; the biller endpoint omits bank fields entirely. You still control the data you send in an enquiry, so minimise it and collect consent. See Privacy and PII. Can I read a customer’s dat"},{"t":"PISP (payment initiation)","u":"/glossary/pisp/","c":"Developer glossary","e":"Glossary","s":"A PISP is a Payment Initiation Service Provider — the open-banking rail that initiates a bank payment directly. Credicorp provisions payment links over PISP.","b":"Definition Under open banking, a PISP initiates a payment straight from the payer's bank account with their authorisation, without a card. Credicorp's payment-links endpoint provisions collections over this rail. In plain terms A way to take a bank payment directly, using open banking instead of a card. Why it matters here Provisioning a PISP link collects money in; it is separate from the money-out gate. See the payment read endpoint."},{"t":"POST /partner/v1/applications","u":"/reference/post-partner-v1-applications/","c":"API reference","e":"Reference","s":"Open a new application on the partner ring. Idempotent with an Idempotency-Key, scoped to applications:write, and sub-limited to 5 req/s because each call opens a decisioning case.","b":"Endpoint MethodPOSTPath/partner/v1/applicationsRingpartner (OAuth)AuthOAuth Request A JSON application body (company details, requested amount and term, the product). Send an Idempotency-Key header so a retry cannot open a duplicate case. Requires the applications:write scope. Response A 201 with the created application's id and initial status. The decision is produced asynchronously — read it at GET /partner/v1/decisions/{id} or, better, subscribe to a decision webhook. A replay carrying the same idempotency key returns the original 201, not a second case. Errors & notes This endpoint carries"},{"t":"POST /partner/v1/decisions/{id}/refresh","u":"/reference/post-partner-v1-decisions-id-refresh/","c":"API reference","e":"Reference","s":"Re-run the decisioning model for an application. Costly, so sub-limited to 1 req/s. Use only when new information materially changes the case.","b":"Endpoint MethodPOSTPath/partner/v1/decisions/{id}/refreshRingpartner (OAuth)AuthOAuth Request Path parameter id. An optional body carrying updated inputs. Requires decisions:write. Send an Idempotency-Key. Response A response indicating a refresh was queued; the new outcome arrives via the decision webhook or a subsequent read. Refresh re-runs the model, which is expensive — do it only when materially new information (updated financials, a corrected figure) would change the result, not speculatively. Errors & notes Sub-limited to 1 req/s — re-running the model is costly. A 409 if a refresh is "},{"t":"POST /partner/v1/identity/checks","u":"/reference/post-partner-v1-identity-checks/","c":"API reference","e":"Reference","s":"Run a KYC/AML identity check on the partner ring. Calls the identity provider, so sub-limited to 5 req/s. Returns a check reference; the result may arrive by webhook.","b":"Endpoint MethodPOSTPath/partner/v1/identity/checksRingpartner (OAuth)AuthOAuth Request A JSON body with the subject's identity details to verify. Requires identity:write and an Idempotency-Key. Response A JSON object with the check's reference and initial status. Because the check calls an external KYC/AML provider, the definitive result may follow asynchronously via a webhook or a subsequent read. Handle a pending state gracefully rather than blocking your flow on a synchronous result. Errors & notes Sub-limited to 5 req/s — each call hits the KYC/AML provider. A 422 for insufficient identity"},{"t":"POST /partner/v1/mcp","u":"/reference/post-partner-v1-mcp/","c":"API reference","e":"Reference","s":"The token-gated MCP server: the same JSON-RPC 2.0 protocol as the public server, behind an OAuth bearer token, exposing authenticated partner capabilities to agents.","b":"Endpoint MethodPOSTPath/partner/v1/mcpRingpartner (OAuth-gated) Parameters A JSON-RPC 2.0 request body (same method set as the public server) with an Authorization: Bearer header carrying a partner access token scoped for MCP use. Response A single JSON-RPC 2.0 response. The tool set is the authenticated partner catalogue rather than the public read-only tools. A client discovers the auth requirements via /.well-known/oauth-protected-resource, obtains a token from the token endpoint, and calls this endpoint. See also the public MCP endpoint. Errors A 401 for a missing or invalid token; a 403 f"},{"t":"POST /partner/v1/oauth/introspect","u":"/reference/post-partner-v1-oauth-introspect/","c":"API reference","e":"Reference","s":"The OAuth 2.0 token-introspection endpoint. Check whether an access token is active and read its scopes, expiry and subject without decoding the JWT yourself.","b":"Endpoint MethodPOSTPath/partner/v1/oauth/introspectRingpartner (OAuth introspection) Parameters Form-encoded body with the token to introspect (and client authentication). Response A JSON object per RFC 7662: active (boolean) and, when active, the token's scope, exp, client_id and related claims. Use introspection when you want the authorization server to be the source of truth for validity — for example to honour a revocation immediately — rather than trusting a still-unexpired JWT you decoded locally. Errors An inactive or unknown token returns active: false (not an error). Client-authentica"},{"t":"POST /partner/v1/oauth/token","u":"/reference/post-partner-v1-oauth-token/","c":"API reference","e":"Reference","s":"The OAuth 2.0 token endpoint. Exchange client credentials for a short-lived bearer access token scoped to the capabilities you request.","b":"Endpoint MethodPOSTPath/partner/v1/oauth/tokenRingpartner (OAuth token endpoint) Parameters Form-encoded body (application/x-www-form-urlencoded):FieldNotesgrant_typeclient_credentialsclient_idYour project's client ID.client_secretYour project's secret (server-side only).scopeSpace-separated scopes; request only what you use. Response A JSON token response: access_token (a bearer JWT), token_type (Bearer), expires_in (seconds) and the granted scope. Cache and reuse the token until just before expiry — see the token lifecycle. Verify the JWT against the JWKS if you validate it yourself. Errors "},{"t":"POST /partner/v1/payments/links","u":"/reference/post-partner-v1-payments-links/","c":"API reference","e":"Reference","s":"Provision a payment link over open-banking PISP on the partner ring. Sub-limited to 10 req/s. Money-out remains a governed manual gate independent of this call.","b":"Endpoint MethodPOSTPath/partner/v1/payments/linksRingpartner (OAuth)AuthOAuth Request A JSON body describing the payment to collect (amount, reference, the payer context). Requires payments:write and an Idempotency-Key. Response A JSON object with the provisioned payment link and its status. The link is created via a PISP (open-banking payment initiation) rail. This endpoint provisions a collection; it does not itself release Credicorp funds — money-out is a separate governed manual gate, the single hard control in the platform. Errors & notes Sub-limited to 10 req/s — each call provisions a P"},{"t":"POST /public/v1/consent","u":"/reference/post-public-v1-consent/","c":"API reference","e":"Reference","s":"Record a PECR cookie-banner consent snapshot from a marketing site's cookie banner. Fail-open, unauthenticated, and distinct from credit-application consent.","b":"Endpoint MethodPOSTPath/public/v1/consentRingpublic (unauthenticated write) Parameters A JSON body with the visitor's cookie-consent choices (analytics and marketing booleans) captured by the site's cookie banner. This is PECR §6 / GDPR cookie consent — not credit-application consent, which lives elsewhere. Response A JSON acknowledgement that the consent snapshot was recorded. The endpoint is fail-open — it never blocks the visitor on a storage failure, so the cookie-banner UX is never held up by the hub. It validates nothing about the caller's identity; it simply records the choice. Callers "},{"t":"POST /public/v1/consent","u":"/reference/post-consent/","c":"API reference","e":"API reference","s":"POST /public/v1/consent records a cookie-consent snapshot under PECR §6 / GDPR recital 47. It captures a visitor’s analytics and marketing cookie choices when they interact with the cookie banner. This is cookie consent, not credit-application consent — the two are stored separately and must not be conflated.","b":"What it does This endpoint sits on the public /public/v1 ring — unauthenticated, anonymous and open to any caller. There is no API key and no OAuth token on this ring; the trust boundary is enforced by rate limiting, strict input validation and a server-fixed response shape rather than by a credential.When a visitor makes a choice in the cookie banner, the marketing site’s server-side client forwards that choice here. The record is appended to a PECR audit trail — consent snapshots are never mutated or deleted, only added, so the estate can always reconstruct exactly what a visitor agreed to a"},{"t":"POST /public/v1/enquiries","u":"/reference/post-public-v1-enquiries/","c":"API reference","e":"Reference","s":"The public lead-intake endpoint: record one accepted enquiry, lead or complaint. Unauthenticated by design (anonymous leads are the point), fail-open, with its own anti-abuse.","b":"Endpoint MethodPOSTPath/public/v1/enquiriesRingpublic (unauthenticated write) Parameters A JSON body describing the enquiry — contact details, message and the consent captured on the form. The exact fields mirror the marketing and help-centre request forms; a valid consent capture is required. Response A JSON acknowledgement that the enquiry was recorded (into the enquiries table). The endpoint is fail-open: it never blocks the visitor on a storage failure, and it returns no stored data back to the caller. The heavyweight anti-abuse stack — honeypot, time-trap, CSRF and edge cap — runs in the "},{"t":"POST /public/v1/enquiries","u":"/reference/post-enquiries/","c":"API reference","e":"API reference","s":"POST /public/v1/enquiries records a public enquiry — the endpoint behind every contact form, department request and complaint intake on the Credicorp estate. It is unauthenticated, rate limited by IP, and returns 201 {id, status} on success. Consent is mandatory: the payload must carry fields.consent = \"yes\" or the request is rejected.","b":"What it does This endpoint sits on the public /public/v1 ring — unauthenticated, anonymous and open to any caller. There is no API key and no OAuth token on this ring; the trust boundary is enforced by rate limiting, strict input validation and a server-fixed response shape rather than by a credential.The enquiry endpoint accepts a completed public form and persists it as a new enquiry record with a server-fixed status of new. It then fans the enquiry out to the correct department inbox (for example support@credicorp.co.uk or complaints@credicorp.co.uk) on a best-effort basis. Mail dispatch is"},{"t":"POST /public/v1/mcp","u":"/reference/post-public-v1-mcp/","c":"API reference","e":"Reference","s":"The MCP server's JSON-RPC 2.0 endpoint: one request in, one response out. Methods include initialize, tools/list, tools/call and ping. No token required.","b":"Endpoint MethodPOSTPath/public/v1/mcpRingpublic (unauthenticated, rate-limited) Parameters A JSON-RPC 2.0 request body. Supported methods:initialize — begin an MCP session; returns server capabilities.notifications/initialized — client-ready notification.ping — liveness.tools/list — enumerate the available tools.tools/call — invoke a tool by name with arguments. Response A single JSON-RPC 2.0 response (MCP Streamable HTTP). For tools/call, the result is the tool's structured output — for example a get_quote result. Invalid parameters surface as a JSON-RPC -32602 error carrying the underlying e"},{"t":"POST /public/v1/mcp","u":"/reference/post-mcp/","c":"API reference","e":"API reference","s":"/public/v1/mcp is the Credicorp MCP server — a Model Context Protocol endpoint that lets an AI agent read public business-lending facts through six read-only tools. The server identifies as Credicorp v1.0.0 on protocol revision 2025-06-18. POST handles JSON-RPC calls; GET returns the server card so a client can inspect capabilities before connecting.","b":"What it does This endpoint sits on the public /public/v1 ring — unauthenticated, anonymous and open to any caller. There is no API key and no OAuth token on this ring; the trust boundary is enforced by rate limiting, strict input validation and a server-fixed response shape rather than by a credential.The MCP endpoint exposes Credicorp’s public business-lending knowledge to AI agents through the Model Context Protocol. Every tool is read-only — the server can describe products, criteria and quotes, but it never writes, applies or moves money. POST carries JSON-RPC requests (initialize, tools/l"},{"t":"POST /public/v1/support/chat","u":"/reference/post-support-chat/","c":"API reference","e":"API reference","s":"POST /public/v1/support/chat powers the embedded support assistant — the endpoint the on-site chat widget calls to send a visitor message and receive a reply. It is public and rate limited by IP, and it is the runtime counterpart to the widget.js/widget.css assets you drop into a page.","b":"What it does This endpoint sits on the public /public/v1 ring — unauthenticated, anonymous and open to any caller. There is no API key and no OAuth token on this ring; the trust boundary is enforced by rate limiting, strict input validation and a server-fixed response shape rather than by a credential.The support chat endpoint accepts a visitor message and returns the assistant’s reply. It is the runtime for the drop-in support widget: you load widget.js and widget.css on your page, and the widget posts here as the visitor types. Request body FieldTypeRequiredNotesmessagestringYesThe visitor’s"},{"t":"Paginate a partner collection","u":"/recipes/paginate-a-partner-collection/","c":"Integration recipes","e":"Recipe","s":"Walk a partner collection with cursor pagination: pass a limit, follow next_cursor until it is null, and page at a sensible size so you do not exhaust your rate bucket.","b":"The loop let cursor = null; do { const url = `.../applications?limit=100${cursor ? `&amp;cursor=${cursor}` : ''}`; const page = await get(url); process(page.items); cursor = page.next_cursor; } while (cursor);Treat next_cursor as opaque and stop when it is null — see the pagination reference. Page at the right size Each page is one request against your bucket, so page at a reasonable limit (tens to a hundred) rather than one item at a time. Cursor paging is stable under concurrent inserts, so you can iterate a large, live collection without skipping or repeating rows. Back off if throttled If "},{"t":"Pagination","u":"/glossary/pagination/","c":"Developer glossary","e":"Glossary","s":"Pagination — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is Pagination breaks a large collection into pages so a client fetches it in chunks rather than all at once, usually with a cursor or page number. In the Credicorp API The public read endpoints return small, complete sets — the four loyalty tiers, the biller list, a single CMS page — so they are not paginated. If you integrate the partner API, larger collections there do use pagination; see the partner pagination docs. Is the biller list paginated? No. The public read endpoints return complete, small sets, so there is nothing to page through. Pagination applies on the larger partner-AP"},{"t":"Pagination on the partner API","u":"/reference/pagination-partner-api/","c":"API reference","e":"Reference","s":"How partner list endpoints paginate: cursor-based paging with a limit and a next cursor, stable ordering, and how to iterate a full collection safely.","b":"Endpoint MethodGETPath/partner/v1/*Ringpartner (list conventions)AuthOAuth Request List endpoints accept a limit and a cursor. The response carries the page of items plus a next_cursor when more remain. Response Partner collections use cursor pagination. Pass limit for the page size and, on each subsequent call, the next_cursor from the previous response. When next_cursor is absent or null, you have reached the end. Cursor paging is stable under concurrent inserts, unlike offset paging, so you never skip or repeat an item mid-iteration. Errors & notes Do not construct cursors yourself — treat "},{"t":"Partner API error reference","u":"/reference/errors-partner-api/","c":"API reference","e":"Reference","s":"The error model for the /partner/v1 ring: auth errors, scope errors, validation, conflict, the 429 with RateLimit headers, OAuth errors and which are retryable.","b":"Endpoint MethodGETPath/partner/v1/*Ringpartner (error model) Parameters Not applicable — this page documents the shared error shape returned across the partner ring. Response StatusCode familyMeaningRetry?400 / 422bad_request / validationMalformed or invalid bodyNo401invalid_tokenMissing / expired / bad tokenRe-mint once, then yes403insufficient_scopeToken lacks the scopeNo — fix the credential404not_foundUnknown resourceNo409conflictDuplicate / state conflictNo — reconcile429rate_limitedBucket empty (see RateLimit-*)Yes — after Reset5xxserver_errorTransient faultYes — back off Errors OAuth er"},{"t":"Partner ring (/partner/v1)","u":"/glossary/partner-ring/","c":"Developer glossary","e":"Glossary","s":"The partner ring is the token-gated /partner/v1 surface — applications, decisions, payments and identity checks — protected by OAuth 2.0 and metered per project.","b":"Definition The /partner/v1 ring is the authenticated integration API. It is where a partner takes applications, reads decisions, provisions payments and runs identity checks — anything that touches a customer, a decision or money. In plain terms The paid, credentialed half of the API where the real integration work happens. Why it matters here Access is via OAuth client credentials and quotas scale by tier. Money-out stays a governed manual gate. See the partner API overview and the public ring."},{"t":"Payment reason: bank_rejected","u":"/reference/payment-reason-bank-rejected/","c":"API reference","e":"API reference","s":"bank_rejected is a payment failure_code value. The bank rejected the collection. It appears in the <a href=\"/reference/event-payment-failed/\">payment.failed</a> webhook payload under failure_code. Ask the customer to check with their bank; offer an alternative payment route.","b":"What it means The bank rejected the collection. A bank-side block, closed account or scheme error. Where you see it It arrives in the webhook payload:{ \"data\": { \"object\": { \"id\": \"pay_7M3X1\", \"loan_id\": \"loan_2K9P4\", \"status\": \"failed\", \"failure_code\": \"bank_rejected\" } } } How to handle it Ask the customer to check with their bank; offer an alternative payment route.Reconcile against live state, since webhooks are unordered and a later event may supersede this one. In practice Treat bank_rejected as one branch of your failure_code handling, not the whole story. For payments, the value tells "},{"t":"Payment reason: disputed_by_customer","u":"/reference/payment-reason-disputed-by-customer/","c":"API reference","e":"API reference","s":"disputed_by_customer is a payment failure_code value. The customer disputed the collection. It appears in the <a href=\"/reference/event-payment-failed/\">payment.failed</a> webhook payload under failure_code. Pause dunning, flag for review, and reconcile once the dispute resolves.","b":"What it means The customer disputed the collection. An indemnity claim or chargeback was raised. Where you see it It arrives in the webhook payload:{ \"data\": { \"object\": { \"id\": \"pay_7M3X1\", \"loan_id\": \"loan_2K9P4\", \"status\": \"failed\", \"failure_code\": \"disputed_by_customer\" } } } How to handle it Pause dunning, flag for review, and reconcile once the dispute resolves.Reconcile against live state, since webhooks are unordered and a later event may supersede this one. In practice Treat disputed_by_customer as one branch of your failure_code handling, not the whole story. For payments, the value "},{"t":"Payment reason: insufficient_funds","u":"/reference/payment-reason-insufficient-funds/","c":"API reference","e":"API reference","s":"insufficient_funds is a payment failure_code value. The account had insufficient funds to cover the collection. It appears in the <a href=\"/reference/event-payment-failed/\">payment.failed</a> webhook payload under failure_code. Respond supportively: a gentle reminder and hardship signposting, not pressure.","b":"What it means The account had insufficient funds to cover the collection. A Direct Debit or card collection that bounced for lack of balance. Where you see it It arrives in the webhook payload:{ \"data\": { \"object\": { \"id\": \"pay_7M3X1\", \"loan_id\": \"loan_2K9P4\", \"status\": \"failed\", \"failure_code\": \"insufficient_funds\" } } } How to handle it Respond supportively: a gentle reminder and hardship signposting, not pressure. The collection is usually re-attempted on schedule.Reconcile against live state, since webhooks are unordered and a later event may supersede this one. In practice Treat insuffici"},{"t":"Payment reason: mandate_cancelled","u":"/reference/payment-reason-mandate-cancelled/","c":"API reference","e":"API reference","s":"mandate_cancelled is a payment failure_code value. The payment mandate had been cancelled. It appears in the <a href=\"/reference/event-payment-failed/\">payment.failed</a> webhook payload under failure_code. Prompt the customer to set up a new mandate before the next due date.","b":"What it means The payment mandate had been cancelled. The customer or their bank cancelled the Direct Debit before collection. Where you see it It arrives in the webhook payload:{ \"data\": { \"object\": { \"id\": \"pay_7M3X1\", \"loan_id\": \"loan_2K9P4\", \"status\": \"failed\", \"failure_code\": \"mandate_cancelled\" } } } How to handle it Prompt the customer to set up a new mandate before the next due date.Reconcile against live state, since webhooks are unordered and a later event may supersede this one. In practice Treat mandate_cancelled as one branch of your failure_code handling, not the whole story. For"},{"t":"Payment reason: technical_error","u":"/reference/payment-reason-technical-error/","c":"API reference","e":"API reference","s":"technical_error is a payment failure_code value. A technical error prevented collection. It appears in the <a href=\"/reference/event-payment-failed/\">payment.failed</a> webhook payload under failure_code. No customer action needed; the collection is re-attempted automatically.","b":"What it means A technical error prevented collection. A transient failure in the payment rails. Where you see it It arrives in the webhook payload:{ \"data\": { \"object\": { \"id\": \"pay_7M3X1\", \"loan_id\": \"loan_2K9P4\", \"status\": \"failed\", \"failure_code\": \"technical_error\" } } } How to handle it No customer action needed; the collection is re-attempted automatically.Reconcile against live state, since webhooks are unordered and a later event may supersede this one. In practice Treat technical_error as one branch of your failure_code handling, not the whole story. For payments, the value tells you w"},{"t":"Polling","u":"/glossary/polling/","c":"Developer glossary","e":"Glossary","s":"Polling — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is Polling means calling an endpoint on a schedule to check for new data, as opposed to receiving a push (a webhook). In the Credicorp API On the public ring there are no webhooks, so polling a cacheable read is the way to pick up changes — but poll sparingly and cache, because the ring is limited to 60 requests per 60 seconds per IP. For a health probe, a call every minute or two is ample. See monitoring. How often should I poll the public API? Sparingly — the ring allows 60 requests per 60 seconds per IP. For most needs a call every minute or two, cached, is plenty."},{"t":"Polling vs webhooks","u":"/glossary/polling-vs-webhooks/","c":"Developer glossary","e":"Glossary","s":"Polling pulls, webhooks push. Polling repeatedly asks the API \"anything new?\"; webhooks have the platform tell you as events happen. Webhooks are lower-latency and cheaper at scale.","b":"Definition Polling and webhooks are the two ways to stay in sync. Polling is simple and needs no public endpoint, but it wastes requests (and your rate-limit budget) and adds latency. Webhooks push each event within seconds, but need a reachable HTTPS endpoint and duplicate-tolerant handling. Use webhooks for timely reactions to events; fall back to polling only when you cannot host an endpoint. See Webhooks explained. Can I use both? Yes, and it is a robust pattern: react to webhooks in real time, and run an occasional reconciliation poll to catch anything missed during an outage. See [Replay"},{"t":"Privacy and PII on the public ring","u":"/public-api/privacy-and-pii-on-the-public-ring/","c":"Public API guides","e":"Public API","s":"No per-customer personal data ever crosses the public ring. Every response is either public vocabulary or a public-safe projection with sensitive fields structurally removed. Submissions carry only what the sender provides, gated by mandatory consent. For a GDPR-conscious integration, the public ring is the low-risk surface — but you still own the data you send to it.","b":"The outbound rule Nothing the public ring returns identifies a specific customer. The loyalty endpoint returns the tier vocabulary, not any person’s tier. The billers endpoint returns public-safe fields with bank and settlement data structurally absent. CMS pages are published marketing copy. There is simply no per-customer data to leak. The inbound rule Two endpoints accept data: enquiries and consent. Both require explicit consent — an enquiry must carry fields.consent = \"yes\", and the consent endpoint exists precisely to record a PECR cookie-consent snapshot into an append-only audit trail."},{"t":"Project","u":"/glossary/project/","c":"Developer glossary","e":"Glossary","s":"A project is the partner unit that owns an OAuth client and a rate-limit bucket. Sandbox and live are separate projects with independent buckets.","b":"Definition A project scopes both a credential and a token bucket. All integrations under one project share its rate limit; sandbox and live are separate projects, so their quotas and data never mix. In plain terms The container that holds your API credentials and your rate-limit allowance. Why it matters here Run one project per workload for isolated headroom, and keep sandbox separate from live. See environments and the sandbox."},{"t":"Proxy the public API from your server","u":"/recipes/proxy-the-public-api-server-side/","c":"Integration recipes","e":"Recipe","s":"Proxy public reads through your backend when you want shared caching, a single egress IP, or to aggregate figures. Mind that the 60 req/60s limit is then keyed to your server's IP.","b":"Why proxy Most public reads (products, pricing, loyalty) are cacheable config. Proxying them through your backend lets you cache once and serve many clients, smoothing load and latency. It also gives you a single point to add attribution logging or aggregate Credicorp figures with your own data. Mind the rate-limit keying The public ring meters 60 req/60s per IP. Behind a proxy, every request shares your server's IP, so a naive pass-through can throttle under load. Cache aggressively on cacheable reads to keep origin calls well under the window, and never proxy a per-keystroke quote without de"},{"t":"Proxy the public API through your backend","u":"/recipes/proxy-the-public-api-through-your-backend/","c":"Integration recipes","e":"Recipe","s":"Put your own backend in front of the public API and everything gets easier. A thin proxy lets you cache reads once for all users, sidestep browser CORS, and apply your own rate control — while keeping the public ring calls server-to-server. It is the recommended shape for anything beyond a trivial browser fetch.","b":"Why proxy A proxy lets you cache the loyalty ladder or CMS pages a single time and serve them to every visitor, avoids CORS entirely (server-to-server calls are not cross-origin in the browser sense), and lets you throttle your own users independently of the public ring’s limit. Keep writes server-side Route submissions (enquiries, consent) through the proxy too, so your edge validates and forwards them — the consent endpoint depends on that trust boundary. Cache with the freshness marker Use the generated/updated timestamps to key your proxy cache. See Caching. Does proxying avoid CORS? Yes. "},{"t":"Public API error reference","u":"/reference/errors-public-api/","c":"API reference","e":"Reference","s":"The error model for the /public/v1 ring: status families, the JSON error body, the 422 range errors on quotes, the 429 rate-limit error and which are retryable.","b":"Endpoint MethodGETPath/public/v1/*Ringpublic (error model) Parameters Not applicable — this page documents the shared error shape returned by every public endpoint. Response StatusCode familyMeaningRetry?400bad_requestMalformed requestNo422out_of_range / validatione.g. quote amount/term outside boundsNo — clamp input404not_foundUnknown pathNo429rate_limitedExceeded 60 req/60s per IPYes — after the window5xxserver_errorTransient hub faultYes — back offThe JSON body carries a machine code and a human message; branch on the code. See errors and retries. Errors Every error is JSON with a stable co"},{"t":"Public API vs partner API: which one do you need?","u":"/public-api/public-api-vs-partner-api/","c":"Public API guides","e":"Public API","s":"Pick the public ring when you only need published figures — products, quotes, loyalty tiers. Pick the partner ring when you need to submit applications, read decisions or move money.","b":"A one-question test Ask one thing: does what you are building touch a customer, a credit decision or a payment? If the answer is no — you are rendering a quote, showing the product ladder, listing loyalty tiers, or answering questions via an agent — the public ring is all you need, and you can start today without applying for anything. If the answer is yes, you need a partner project and an OAuth credential. Side by side Concernpublic/v1partner/v1AuthNone (reads)OAuth 2.0 client credentialsRate limit60 req / 60s per IPToken-bucket, scales with tierDataPublished figures onlyCustomer + decision "},{"t":"Public ring","u":"/glossary/the-public-ring/","c":"Developer glossary","e":"Glossary","s":"Public ring — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is The public ring is the outermost trust tier of the Credicorp API, served under /public/v1. Any caller can reach it with no API key and no OAuth token. It exposes public information — products, the loyalty ladder, published pages — and a small set of safe, validated submissions such as enquiries and cookie consent. How it stays safe Without a credential, the ring relies on rate limiting (60 requests per 60 seconds per IP), strict input validation, and server-fixed response shapes. A hard rule underpins it: no per-customer PII ever crosses the public ring. Account data, offers, paymen"},{"t":"Public ring (/public/v1)","u":"/glossary/public-ring/","c":"Developer glossary","e":"Glossary","s":"The public ring is the unauthenticated /public/v1 surface — published figures, quotes, loyalty vocabulary, a lead endpoint and an MCP server, all callable without a token.","b":"Definition The /public/v1 ring is Credicorp's unauthenticated, read-mostly API surface. It publishes only non-sensitive data — products, pricing, quotes, the loyalty ladder — plus two fail-open write endpoints (enquiries and consent) and an MCP server. In plain terms The part of the API you can call without any credential, because everything it returns is already public. Why it matters here It is where comparison sites and AI agents integrate, and it is metered at 60 requests per 60 seconds per IP. Contrast the partner ring. Full tour: the public API overview."},{"t":"Quickstart: Add metrics and tracing to your API calls","u":"/recipes/quickstart-observe-api-calls-with-metrics/","c":"Integration recipes","e":"Quickstart","s":"Wrap every call in a span and a timer, tag it with the request_id, and you get latency, error-rate and end-to-end tracing for free. This recipe gives you the exact code, uses only the unauthenticated public ring, and links to the endpoints and the application flow so the reader always has a next step.","b":"Instrument the client async function tracedCall(url, init) { const span = tracer.startSpan(&#x27;credicorp.call&#x27;, { attributes: { &#x27;http.url&#x27;: url } }); const start = performance.now(); try { const res = await fetch(url, init); span.setAttribute(&#x27;credicorp.request_id&#x27;, res.headers.get(&#x27;X-Request-Id&#x27;)); span.setAttribute(&#x27;http.status_code&#x27;, res.status); metrics.histogram(&#x27;credicorp.latency&#x27;, performance.now() - start); if (!res.ok) metrics.increment(&#x27;credicorp.errors&#x27;); return res; } finally { span.end(); } } Correlate with support"},{"t":"Quickstart: Build a live repayment illustration","u":"/recipes/quickstart-build-a-repayment-illustration/","c":"Integration recipes","e":"Quickstart","s":"A live illustration that quotes as the user moves a slider feels great — debounce it so you respect the rate limit. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"Debounce the quote call let t; slider.addEventListener(&#x27;input&#x27;, () =&gt; { clearTimeout(t); t = setTimeout(async () =&gt; { const res = await fetch(`${BASE}/quote`, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: Number(slider.value), term_months: 6 }), }); const q = await res.json(); output.textContent = `\\u00a3${q.repayment}/month`; }, 300); });A 300ms debounce means one call per pause, not one per pixel — smooth for the user and easy on the limit. Prefer client-side maths where you can If you fetch pri"},{"t":"Quickstart: Build a product comparison table","u":"/recipes/quickstart-build-a-product-comparison-table/","c":"Integration recipes","e":"Quickstart","s":"Rendering a comparison table straight from GET /public/v1/products gives readers an always-current view of what Credicorp offers. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"Fetch and render Map the product array into rows and add a click-to-sort header. The data is already structured for a table:const { data } = await (await fetch(`${BASE}/products`)).json(); data.sort((a, b) =&gt; a.representative_apr - b.representative_apr); table.tBodies[0].innerHTML = data.map(p =&gt; `&lt;tr&gt;&lt;td&gt;${p.name}&lt;/td&gt;` + `&lt;td data-sort=&quot;${p.max_amount}&quot;&gt;\\u00a3${p.max_amount.toLocaleString()}&lt;/td&gt;` + `&lt;td data-sort=&quot;${p.representative_apr}&quot;&gt;${p.representative_apr}%&lt;/td&gt;&lt;/tr&gt;` ).join(&#x27;&#x27;); Keep it honest Label r"},{"t":"Quickstart: Cache the product catalogue correctly","u":"/recipes/quickstart-cache-the-product-catalogue/","c":"Integration recipes","e":"Quickstart","s":"The catalogue changes rarely, so caching it removes the API from your hot path and keeps you well under the rate limit. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"Honour the header Read Cache-Control: max-age on the response and cache for exactly that long. Do not invent your own TTL that ignores what the API tells you.resp = requests.get(f&#x27;{BASE}/products&#x27;, timeout=10) max_age = parse_max_age(resp.headers.get(&#x27;Cache-Control&#x27;, &#x27;&#x27;)) cache.set(&#x27;products&#x27;, resp.json()[&#x27;data&#x27;], ttl=max_age or 3600) Stale-while-revalidate Serve the cached copy immediately and refresh in the background when it is near expiry. The reader never waits on the API, and a transient failure just serves slightly older data — far bette"},{"t":"Quickstart: Credicorp public API for a B2B marketplace","u":"/recipes/quickstart-for-marketplace/","c":"Integration recipes","e":"Quickstart","s":"You run a B2B marketplace and want to offer financing to buyers or sellers at the point of need. This quickstart shows the exact endpoints and code, uses the unauthenticated public ring, and always ends with a clean handoff to the real application — Credicorp lends to UK limited companies with no personal guarantee.","b":"Finance at the point of need At basket or invoice stage, quote the order value with the quote endpoint and show 'spread this cost' with a real monthly figure. Hand the buyer off Submit an enquiry with the order context and redirect to the handoff_url. Credicorp lends to the buyer's limited company, keeping your marketplace out of the credit relationship.async function financeOrder(order) { const res = await fetch(`${BASE}/enquiries`, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27;, &#x27;Idempotency-Key&#x27;: order.id }, body: JSON.stringify({ com"},{"t":"Quickstart: Credicorp public API for a broker or introducer CRM","u":"/recipes/quickstart-for-broker-crm/","c":"Integration recipes","e":"Quickstart","s":"You are a commercial-finance broker and want to pre-qualify and submit clients without leaving your CRM. This quickstart shows the exact endpoints and code, uses the unauthenticated public ring, and always ends with a clean handoff to the real application — Credicorp lends to UK limited companies with no personal guarantee.","b":"Pre-qualify in the pipeline Add a Credicorp panel to your deal view that calls the quote endpoint with the client's requested amount and term, so your broker sees an indicative repayment before they pitch it. Submit the enquiry When the client agrees, submit an enquiry from the deal record with an idempotency key so a double-click never creates two leads:$res = Http::withHeaders([&#x27;Idempotency-Key&#x27; =&gt; $deal-&gt;uuid]) -&gt;post($base.&#x27;/enquiries&#x27;, $deal-&gt;toEnquiryPayload()); $deal-&gt;update([&#x27;handoff_url&#x27; =&gt; $res-&gt;json(&#x27;handoff_url&#x27;)]); Track"},{"t":"Quickstart: Credicorp public API for a business-finance comparison site","u":"/recipes/quickstart-for-comparison-site/","c":"Integration recipes","e":"Quickstart","s":"You are running a comparison or aggregator site and want Credicorp in your line-up. This quickstart shows the exact endpoints and code, uses the unauthenticated public ring, and always ends with a clean handoff to the real application — Credicorp lends to UK limited companies with no personal guarantee.","b":"Populate the comparison Pull the live catalogue with the products endpoint and render Credicorp alongside your other providers. Because the data is live, your listing never goes stale:const { data } = await (await fetch(`${BASE}/products`)).json(); addProviderRows(&#x27;Credicorp&#x27;, data); Show honest figures Display representative_apr and the amount range from the product object, and offer a live quote for a specific amount. Label everything representative — a firm price comes from applying. Hand off the lead When a user picks Credicorp, submit an enquiry and redirect them to the handoff_"},{"t":"Quickstart: Credicorp public API for a digital agency building for clients","u":"/recipes/quickstart-for-agency-white-label/","c":"Integration recipes","e":"Quickstart","s":"You are an agency building sites for business clients and want a reusable funding widget. This quickstart shows the exact endpoints and code, uses the unauthenticated public ring, and always ends with a clean handoff to the real application — Credicorp lends to UK limited companies with no personal guarantee.","b":"Build it once Wrap the product picker and a repayment illustration into a single script you configure per client with data attributes:&lt;div data-credicorp-widget data-amount=&quot;25000&quot; data-theme=&quot;client-a&quot;&gt;&lt;/div&gt; &lt;script src=&quot;https://cdn.youragency.com/credicorp-widget.js&quot; defer&gt;&lt;/script&gt; Ship it everywhere Because the public ring needs no keys, the same widget drops into any client site with only a theme change. Each client's readers land on apply when they are ready. Do I need separate credentials per client? No — the public ring needs none."},{"t":"Quickstart: Credicorp public API for a native mobile app","u":"/recipes/quickstart-for-mobile-app/","c":"Integration recipes","e":"Quickstart","s":"You are building a native app and want to offer Credicorp funding to your business users. This quickstart shows the exact endpoints and code, uses the unauthenticated public ring, and always ends with a clean handoff to the real application — Credicorp lends to UK limited companies with no personal guarantee.","b":"Proxy, don't call direct Route API calls through your own backend rather than embedding logic in the app, so you can cache, shape payloads and later add authenticated partner calls without shipping secrets in the binary. See proxying. Hand off in a web view Submit an enquiry from your backend and open the returned handoff_url in an in-app browser or SFSafariViewController, so the secure application runs on the web where it belongs. Cache for offline Cache the product catalogue so the funding screen still renders when the device is briefly offline. Should the app call the API directly? Prefer p"},{"t":"Quickstart: Credicorp public API for a no-code / automation workflow","u":"/recipes/quickstart-for-no-code-zapier/","c":"Integration recipes","e":"Quickstart","s":"You want to connect a form or CRM to Credicorp without writing a service, using a no-code automation tool. This quickstart shows the exact endpoints and code, uses the unauthenticated public ring, and always ends with a clean handoff to the real application — Credicorp lends to UK limited companies with no personal guarantee.","b":"Configure a webhook action In Zapier, Make or n8n, add a 'Webhook / HTTP POST' step pointing at the enquiries endpoint. Map your form fields to the enquiry payload and set the headers:{ &quot;url&quot;: &quot;https://api.credicorp.co.uk/public/v1/enquiries&quot;, &quot;method&quot;: &quot;POST&quot;, &quot;headers&quot;: { &quot;Content-Type&quot;: &quot;application/json&quot;, &quot;Idempotency-Key&quot;: &quot;{{submission_id}}&quot; }, &quot;body&quot;: { &quot;company&quot;: { &quot;name&quot;: &quot;{{company_name}}&quot; }, &quot;contact&quot;: { &quot;email&quot;: &quot;{{email}}&quot; "},{"t":"Quickstart: Credicorp public API for an accountancy client portal","u":"/recipes/quickstart-for-accountancy-portal/","c":"Integration recipes","e":"Quickstart","s":"You run an accountancy practice portal and want to surface business-funding options to clients. This quickstart shows the exact endpoints and code, uses the unauthenticated public ring, and always ends with a clean handoff to the real application — Credicorp lends to UK limited companies with no personal guarantee.","b":"Show a funding option Add a funding card to the client dashboard that lists Credicorp products via the products endpoint and offers an indicative quote for the client's figure. Keep disclosures correct Pull the representative example and any risk warning from the CMS by key so your portal's disclosures always match the source — important for a regulated adviser. Refer, don't advise Present the option and link to apply; the client completes the application directly. Your clients are UK limited companies, which is exactly who Credicorp lends to. See the sector guide for accountancy practices. Is"},{"t":"Quickstart: Credicorp public API for an e-commerce or SaaS platform","u":"/recipes/quickstart-for-ecommerce-platform/","c":"Integration recipes","e":"Quickstart","s":"You run a platform whose merchants are limited companies, and you want to offer them working capital. This quickstart shows the exact endpoints and code, uses the unauthenticated public ring, and always ends with a clean handoff to the real application — Credicorp lends to UK limited companies with no personal guarantee.","b":"Add a funding CTA Place a 'Grow your business' card in your merchant dashboard. Use pricing config to show a realistic headline and pre-fill a sensible amount based on the merchant's turnover on your platform. Contextual quote const suggested = Math.min(merchant.monthlyRevenue * 2, 150000); const q = await quote({ amount: suggested, term_months: 6 }); showFundingCard(`Borrow up to \\u00a3${suggested.toLocaleString()} \\u2014 from \\u00a3${q.repayment}/mo`); Hand off cleanly Submit an enquiry pre-filled from the merchant's profile and send them to the handoff_url. This is the classic embedded-fina"},{"t":"Quickstart: Cut bandwidth with conditional requests","u":"/recipes/quickstart-cache-with-etag-conditional-requests/","c":"Integration recipes","e":"Quickstart","s":"When the API sends an ETag, a conditional request lets you skip the body entirely if nothing changed — a 304 and no payload. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"Send the ETag back etag = cache.get(&#x27;products_etag&#x27;) headers = {&#x27;If-None-Match&#x27;: etag} if etag else {} r = requests.get(f&#x27;{BASE}/products&#x27;, headers=headers, timeout=10) if r.status_code == 304: return cache.get(&#x27;products&#x27;) # unchanged r.raise_for_status() cache.set(&#x27;products_etag&#x27;, r.headers.get(&#x27;ETag&#x27;)) cache.set(&#x27;products&#x27;, r.json()[&#x27;data&#x27;]) When it helps Conditional requests shine when you poll reference data that rarely changes. You still make the request, but a 304 skips the body, saving bandwidth and parsing "},{"t":"Quickstart: Debug a failing public API call","u":"/recipes/quickstart-debug-a-failing-api-call/","c":"Integration recipes","e":"Quickstart","s":"When a call fails, work from the network inward: prove reachability, read the exact status and error code, then reproduce with a bare curl. This recipe gives you the exact code, uses only the unauthenticated public ring, and links to the endpoints and the application flow so the reader always has a next step.","b":"Isolate the layer Run healthz first. A non-200 there means the problem is network, proxy or TLS, not your request. If healthz is fine, the issue is in the specific call. Read the real error curl -v -X POST https://api.credicorp.co.uk/public/v1/quote \\ -H &#x27;Content-Type: application/json&#x27; \\ -d &#x27;{&quot;amount&quot;: 999999999}&#x27;-v shows the status line, response headers (including X-Request-Id) and the error envelope. A 400 with invalid_amount tells you exactly what to fix. Common causes Wrong host (production vs sandbox) — check your base URL.Missing Content-Type: application/"},{"t":"Quickstart: Format amounts as GBP for UK readers","u":"/recipes/quickstart-localise-currency-and-formatting/","c":"Integration recipes","e":"Quickstart","s":"Amounts come back as plain numbers in GBP; format them with the platform's own number formatter, not string concatenation. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"Format in JavaScript const gbp = new Intl.NumberFormat(&#x27;en-GB&#x27;, { style: &#x27;currency&#x27;, currency: &#x27;GBP&#x27;, maximumFractionDigits: 0, }); gbp.format(product.max_amount); // &quot;\\u00a3150,000&quot; Format in PHP and Python $fmt = new NumberFormatter(&#x27;en_GB&#x27;, NumberFormatter::CURRENCY); echo $fmt-&gt;formatCurrency($product[&#x27;max_amount&#x27;], &#x27;GBP&#x27;); // \\u00a3150,000import babel.numbers babel.numbers.format_currency(product[&#x27;max_amount&#x27;], &#x27;GBP&#x27;, locale=&#x27;en_GB&#x27;) Amounts vs repayments Product min_amount/max_amount ar"},{"t":"Quickstart: Gate features on live API status","u":"/recipes/quickstart-gate-features-on-api-status/","c":"Integration recipes","e":"Quickstart","s":"Read the status endpoint on a schedule and gate live-API features off the cached result, so an outage degrades quietly. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"Cache status, gate features let apiHealthy = true; setInterval(async () =&gt; { try { const { status } = await (await fetch(`${BASE}/status`)).json(); apiHealthy = status === &#x27;operational&#x27;; } catch { apiHealthy = false; } }, 60_000); function renderQuoteWidget() { return apiHealthy ? liveQuoteWidget() : cachedProductLinks(); } Fail soft, not loud When the API is impaired, show cached products and a link to business loans instead of a broken widget. The reader may never notice. Choose what to gate Not every feature needs gating. Static reference data you have already cached keeps work"},{"t":"Quickstart: Generate a client from the OpenAPI description","u":"/recipes/quickstart-generate-a-client-from-openapi/","c":"Integration recipes","e":"Quickstart","s":"Generating your client from the OpenAPI spec means your types can never drift from the API — regenerate on every contract change. This recipe gives you the exact code, uses only the unauthenticated public ring, and links to the endpoints and the application flow so the reader always has a next step.","b":"Generate the client # TypeScript, using openapi-typescript npx openapi-typescript \\ https://dev.credicorp.co.uk/openapi.json \\ --output src/credicorp.d.tsPoint your generator (openapi-typescript, openapi-generator, oapi-codegen for Go, and so on) at the published description to produce models and a client that match the API exactly. Keep it fresh in CI Regenerate on a schedule or on a contract-changed signal and fail the build if the generated output changes unexpectedly — that diff is your early warning that the API contract moved. Where is the OpenAPI description? Published on the developer "},{"t":"Quickstart: Keep partner secrets off the client","u":"/recipes/quickstart-secure-partner-secrets-server-side/","c":"Integration recipes","e":"Quickstart","s":"The single rule that matters when you graduate from public reads to partner writes: a client secret must never reach the browser. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"The rule A partner OAuth2 client_secret in front-end code is compromised the instant the page loads — anyone can read the bundle. Every authenticated call goes through your backend, which holds the secret and exchanges it for a token server-side. Where to store it A secrets manager (AWS Secrets Manager, Vault, Doppler) in production.An encrypted environment variable read at boot — never a client-exposed one.Never in the repo, never in a VITE_/REACT_APP_ variable, never in a Worker script body (use a secret binding). The safe shape Browser → your backend → Credicorp partner ring. Your backend a"},{"t":"Quickstart: Keep regulated copy in sync with the CMS","u":"/recipes/quickstart-keep-copy-in-sync-with-cms/","c":"Integration recipes","e":"Quickstart","s":"Regulated copy must match the source exactly; fetch it by key and refresh when updated_at changes rather than pasting and forgetting. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"Fetch and cache by key def get_copy(key): cached = cache.get(f&#x27;cms:{key}&#x27;) resp = requests.get(f&#x27;{BASE}/cms/pages/{key}&#x27;, timeout=10).json() if not cached or cached[&#x27;updated_at&#x27;] != resp[&#x27;updated_at&#x27;]: cache.set(f&#x27;cms:{key}&#x27;, resp) return resp[&#x27;body&#x27;] Why this matters for compliance A representative example or risk warning that drifts out of date is a compliance problem. Pulling it by key means a change we make propagates to your page automatically, with an audit trail via updated_at. Which keys can I fetch? The keys documented in the"},{"t":"Quickstart: Localise copy and figures for UK readers","u":"/recipes/quickstart-localise-the-integration-for-uk-english/","c":"Integration recipes","e":"Quickstart","s":"Credicorp is a UK lender to limited companies, so your integration's copy and figures should be unambiguously British. This recipe gives you the exact code, uses only the unauthenticated public ring, and links to the endpoints and the application flow so the reader always has a next step.","b":"Get the details right Format money with en-GB and the GBP currency (see currency formatting).Use British spelling in your own copy: 'organise', 'colour', 'programme'.Frame the offer as business finance for limited companies, never a personal loan.Prefer day-month-year date order in any dates you display. Reflect the lending model Credicorp lends to the company with no personal guarantee. Your copy should say so plainly — it is a genuine differentiator for the director reading it. Link to business loans for the full picture. Words to avoid Some terms carry the wrong meaning for a UK business au"},{"t":"Quickstart: Log requests with the request id","u":"/recipes/quickstart-log-requests-with-request-id/","c":"Integration recipes","e":"Quickstart","s":"The request_id on every response is your thread back to a specific call — log it structured, on success and failure alike. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"Log it structured const res = await fetch(url, init); const reqId = res.headers.get(&#x27;X-Request-Id&#x27;); logger.info(&#x27;credicorp.call&#x27;, { url, status: res.status, request_id: reqId, });Emit key/value fields, not a concatenated string, so you can filter by request_id or status later. Use it with support When something looks wrong, quote the request_id in your ticket. It lets the team pull your exact request and response, which turns a vague report into a five-minute fix. Where is the request id? In the JSON body as `request_id` and in the `X-Request-Id` response header. They matc"},{"t":"Quickstart: Make your finance widget accessible","u":"/recipes/quickstart-make-the-finance-widget-accessible/","c":"Integration recipes","e":"Quickstart","s":"A funding widget must be usable by everyone — semantic HTML, keyboard support and a live region for the quote result get you to WCAG. This recipe gives you the exact code, uses only the unauthenticated public ring, and links to the endpoints and the application flow so the reader always has a next step.","b":"Build it semantically &lt;form aria-label=&quot;Business finance quote&quot;&gt; &lt;label for=&quot;amt&quot;&gt;Amount&lt;/label&gt; &lt;input id=&quot;amt&quot; type=&quot;number&quot; min=&quot;1000&quot; max=&quot;150000&quot; /&gt; &lt;button type=&quot;submit&quot;&gt;Get quote&lt;/button&gt; &lt;output id=&quot;result&quot; aria-live=&quot;polite&quot;&gt;&lt;/output&gt; &lt;/form&gt;Use a real &lt;form&gt;, &lt;label&gt;, &lt;button&gt; and &lt;output&gt;. The aria-live=\"polite\" region announces the quote to screen readers when it updates. Focus and contrast Keep a visible focus ring,"},{"t":"Quickstart: Mock the public API in unit tests","u":"/recipes/quickstart-mock-the-api-in-unit-tests/","c":"Integration recipes","e":"Quickstart","s":"For fast unit tests, mock the API with a recorded fixture instead of a live call — reserve the sandbox for integration tests. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"Record a fixture Capture one real sandbox response and save it. Your test then serves that fixture instead of calling out:import { vi } from &#x27;vitest&#x27;; import productsFixture from &#x27;./fixtures/products.json&#x27;; vi.stubGlobal(&#x27;fetch&#x27;, vi.fn(async () =&gt; new Response( JSON.stringify(productsFixture), { status: 200 }, ))); Keep fixtures fresh Refresh fixtures from the sandbox on a schedule so they don't drift from the real contract. A weekly job that re-records and diffs is enough to catch breaking changes early. Mock or hit the sandbox? Mock in unit tests for speed an"},{"t":"Quickstart: Optimise your finance widget's performance","u":"/recipes/quickstart-optimise-widget-performance/","c":"Integration recipes","e":"Quickstart","s":"A funding widget should add near-zero weight — defer the script, cache the data, reserve its space and lazy-load it below the fold. This recipe gives you the exact code, uses only the unauthenticated public ring, and links to the endpoints and the application flow so the reader always has a next step.","b":"Load without blocking &lt;div id=&quot;cc-finance&quot; style=&quot;min-height:120px&quot;&gt;&lt;/div&gt; &lt;script src=&quot;/cc-widget.js&quot; defer&gt;&lt;/script&gt;defer keeps the script off the critical path, and a reserved min-height prevents cumulative layout shift when the content arrives. Lazy-load below the fold new IntersectionObserver((entries, io) =&gt; { if (entries[0].isIntersecting) { initWidget(); io.disconnect(); } }).observe(document.getElementById(&#x27;cc-finance&#x27;));If the widget sits below the fold, only initialise it when it is about to scroll into view. Combine"},{"t":"Quickstart: Proxy the public API through your backend","u":"/recipes/quickstart-proxy-the-public-api-through-your-backend/","c":"Integration recipes","e":"Quickstart","s":"A thin backend proxy lets you cache, trim payloads and keep one origin for your front-end — and it is the seam where partner calls stay server-side. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"A minimal proxy app.get(&#x27;/api/products&#x27;, async (req, res) =&gt; { const upstream = await fetch(`${process.env.CREDICORP_BASE}/products`); const { data } = await upstream.json(); const slim = data.map(({ id, name, min_amount, max_amount }) =&gt; ({ id, name, min_amount, max_amount })); res.set(&#x27;Cache-Control&#x27;, &#x27;public, max-age=3600&#x27;).json(slim); }); Why proxy at all Proxying gives you caching, payload shaping and a single origin for your SPA. Crucially, it is the natural home for authenticated partner calls later — the secret lives on the server and never reaches t"},{"t":"Quickstart: Rate-limit your own widget's API usage","u":"/recipes/quickstart-rate-limit-your-own-widget/","c":"Integration recipes","e":"Quickstart","s":"A busy page can flood the API on its own; debounce writes, cache reads and coalesce duplicate in-flight calls to stay a good citizen. This recipe gives you the exact code, uses only the unauthenticated public ring, and links to the endpoints and the application flow so the reader always has a next step.","b":"Coalesce duplicate requests const inflight = new Map(); function dedupe(key, fn) { if (inflight.has(key)) return inflight.get(key); const promise = fn().finally(() =&gt; inflight.delete(key)); inflight.set(key, promise); return promise; } // two components asking for products share one call dedupe(&#x27;products&#x27;, () =&gt; fetch(`${BASE}/products`).then(r =&gt; r.json())); Debounce and cache Debounce the quote calls so a slider fires once per pause, and cache the catalogue so multiple widgets on a page share one fetch. Together these keep even a heavy page comfortably under the limit. Can"},{"t":"Quickstart: Replace a hard-coded price table with live data","u":"/recipes/quickstart-migrate-from-a-hardcoded-price-table/","c":"Integration recipes","e":"Quickstart","s":"Replacing a stale hand-maintained price table with live API data is low-risk if you fetch behind a flag, diff first, then cut over. This recipe gives you the exact code, uses only the unauthenticated public ring, and links to the endpoints and the application flow so the reader always has a next step.","b":"Fetch behind a flag Add the live catalogue fetch behind a feature flag so you can enable it for a fraction of traffic and watch for issues before a full cut-over. Diff against your static table const live = (await fetch(`${BASE}/products`).then(r =&gt; r.json())).data; const drift = live.filter(l =&gt; static[l.id]?.max_amount !== l.max_amount); if (drift.length) console.warn(&#x27;price table was stale:&#x27;, drift);The diff usually reveals your static table had drifted — which is exactly why you are moving to live data. Keep the static copy as a fallback Retain the old table as the fallback"},{"t":"Quickstart: Serve the catalogue behind your CDN","u":"/recipes/quickstart-serve-the-catalogue-behind-a-cdn/","c":"Integration recipes","e":"Quickstart","s":"Put your CDN in front of the catalogue with a stale-while-revalidate policy and readers get edge-speed data while the API sees almost no traffic. This recipe gives you the exact code, uses only the unauthenticated public ring, and links to the endpoints and the application flow so the reader always has a next step.","b":"Cache at the edge Have a small origin route fetch and normalise the catalogue, then set caching headers your CDN honours:Cache-Control: public, max-age=3600, stale-while-revalidate=86400The CDN serves the cached copy for an hour, then serves slightly stale data while it refreshes in the background — the reader never waits and the API is shielded. Purge on change If you need instant updates, purge the CDN key when you detect a catalogue change (via ETag polling). Otherwise the max-age handles freshness. Which caching header matters? `Cache-Control` with `max-age` and `stale-while-revalidate`. T"},{"t":"Quickstart: Test your integration against the sandbox","u":"/recipes/quickstart-test-against-the-sandbox/","c":"Integration recipes","e":"Quickstart","s":"The sandbox returns deterministic data, so you can assert on exact values in CI without your tests flaking. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"Point tests at sandbox BASE = &#x27;https://sandbox.credicorp.co.uk/public/v1&#x27; def test_products_have_amount_range(): data = requests.get(f&#x27;{BASE}/products&#x27;).json()[&#x27;data&#x27;] assert data, &#x27;catalogue should not be empty&#x27; for p in data: assert p[&#x27;min_amount&#x27;] &lt; p[&#x27;max_amount&#x27;] Snapshot with confidence Because sandbox fixtures are stable, you can snapshot a full response and assert it byte-for-byte. If a snapshot changes unexpectedly, the API contract changed — exactly what you want a test to catch. Never test against production Production d"},{"t":"Quickstart: Understand webhooks: public vs partner","u":"/recipes/quickstart-handle-webhooks-note-public-vs-partner/","c":"Integration recipes","e":"Quickstart","s":"The public ring is request/response only; webhooks live on the authenticated partner ring, because event callbacks carry account data. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"Why the public ring has no webhooks Webhooks push account-specific events — a decision, a payment — to your server. Those events belong to an authenticated relationship, so they are delivered on the partner ring, not the anonymous public ring. What to do on the public ring For public-ring integrations you drive the interaction: submit an enquiry and follow the handoff_url. If you need event callbacks, that is the signal to onboard to the partner programme. Polling as a stopgap Where you only need to know whether reference data changed, poll politely with conditional requests rather than expect"},{"t":"Quickstart: Validate a UK company number before enquiry","u":"/recipes/quickstart-validate-a-uk-company-number/","c":"Integration recipes","e":"Quickstart","s":"Validating the company number before you POST an enquiry saves a round-trip and gives the applicant instant, friendly feedback. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"Basic format check A UK company number is eight characters: eight digits, or two letters followed by six digits (for example SC123456). Check the shape before you submit:const ok = /^(?:\\d{8}|[A-Z]{2}\\d{6})$/.test(number.toUpperCase()); if (!ok) showError(&#x27;Enter a valid 8-character company number&#x27;); Let the API be the authority Client-side validation improves UX but is not authoritative. The enquiry endpoint validates properly and returns a 400 with a field-level error if the number is wrong — always handle that path too. Normalise before you check Users paste company numbers with sp"},{"t":"Quickstart: Wire a support chat into your own UI","u":"/recipes/quickstart-stream-support-chat-replies/","c":"Integration recipes","e":"Quickstart","s":"Building your own chat UI on the support endpoint is straightforward: post a message, keep the conversation_id, render the reply. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"The message loop let conversationId; async function send(text) { appendBubble(&#x27;user&#x27;, text); // optimistic const res = await fetch(`${BASE}/support/chat`, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ conversation_id: conversationId, message: text }), }); const { reply, conversation_id } = await res.json(); conversationId = conversation_id; appendBubble(&#x27;assistant&#x27;, reply); } Handle failures gracefully If a send fails, mark the user bubble as unsent and offer a retry rather than losing the message. Wr"},{"t":"Quickstart: Wrap the public API in your GraphQL schema","u":"/recipes/quickstart-wrap-the-api-in-graphql/","c":"Integration recipes","e":"Quickstart","s":"If your app speaks GraphQL, wrap the REST public ring in resolvers so your front-end queries products and quotes in your own graph. This recipe gives you the exact code, uses only the unauthenticated public ring, and links to the endpoints and the application flow so the reader always has a next step.","b":"Resolve from REST const resolvers = { Query: { products: () =&gt; fetch(`${BASE}/products`).then(r =&gt; r.json()).then(b =&gt; b.data), quote: (_, { amount, termMonths }) =&gt; fetch(`${BASE}/quote`, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount, term_months: termMonths }), }).then(r =&gt; r.json()), }, }; Batch and cache Use a per-request DataLoader to dedupe product lookups within one query, and cache the catalogue at the resolver so repeated queries do not each hit the REST API. Why wrap REST in GraphQL? To gi"},{"t":"Quickstart: Write a typed client in TypeScript","u":"/recipes/quickstart-write-a-typed-client-in-typescript/","c":"Integration recipes","e":"Quickstart","s":"A handful of interfaces gives you end-to-end type safety over the public API — products, quotes and the shared error envelope. This recipe shows the exact code, uses only the unauthenticated public ring, and links out to the endpoints and the application flow so the reader always has a next step.","b":"Model the responses interface Product { id: string; name: string; type: &#x27;flex&#x27; | &#x27;onetime&#x27;; min_amount: number; max_amount: number; representative_apr: number; } interface ApiError { error: { type: string; code: string; message: string; request_id: string }; } async function listProducts(base: string): Promise&lt;Product[]&gt; { const res = await fetch(`${base}/products`); const body = await res.json(); if (!res.ok) throw new Error((body as ApiError).error.code); return (body as { data: Product[] }).data; } Generate types from the schema If you consume many endpoints, gener"},{"t":"Quickstart: add a Credicorp finance option to a EV charge point installer website","u":"/recipes/quickstart-embed-finance-option-for-EV-charge-point-installer/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a EV charge point installer website is one client-side snippet with no backend. Fund units and labour up front while you wait on grant and client payments. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a HR consultancy website","u":"/recipes/quickstart-embed-finance-option-for-HR-consultancy/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a HR consultancy website is one client-side snippet with no backend. Hire and invest in tooling ahead of a signed retainer. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a HVAC contractor website","u":"/recipes/quickstart-embed-finance-option-for-HVAC-contractor/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a HVAC contractor website is one client-side snippet with no backend. Fund parts and a second van as installation demand grows. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a IT support company website","u":"/recipes/quickstart-embed-finance-option-for-IT-support-company/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a IT support company website is one client-side snippet with no backend. Stock hardware and fund a hire ahead of a managed-services contract. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a SEO agency website","u":"/recipes/quickstart-embed-finance-option-for-SEO-agency/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a SEO agency website is one client-side snippet with no backend. Fund a hire and tooling ahead of a won retainer. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a accountancy practice website","u":"/recipes/quickstart-embed-finance-option-for-accountants/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a accountancy practice website takes one client-side snippet and no backend. Smooth the January cash trough and invest in practice software. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet. Pre-set a sensible amount for the sector and let the reader adjust:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;"},{"t":"Quickstart: add a Credicorp finance option to a aesthetic clinic website","u":"/recipes/quickstart-embed-finance-option-for-aesthetic-clinics/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a aesthetic clinic website takes one client-side snippet and no backend. Spread the cost of devices and consumables against booked demand. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet. Pre-set a sensible amount for the sector and let the reader adjust:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;"},{"t":"Quickstart: add a Credicorp finance option to a agricultural contractor website","u":"/recipes/quickstart-embed-finance-option-for-agricultural-contractors/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a agricultural contractor website takes one client-side snippet and no backend. Fund machinery and fuel across an uneven seasonal income. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet. Pre-set a sensible amount for the sector and let the reader adjust:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;"},{"t":"Quickstart: add a Credicorp finance option to a agriculture business website","u":"/recipes/quickstart-embed-finance-option-for-agriculture-business/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a agriculture business website is one client-side snippet with no backend. Bridge the gap between input costs and harvest income. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a amazon seller website","u":"/recipes/quickstart-embed-finance-option-for-amazon-seller/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a amazon seller website is one client-side snippet with no backend. Fund inventory ahead of peak selling seasons without stalling cash flow. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a animation studio website","u":"/recipes/quickstart-embed-finance-option-for-animation-studio/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a animation studio website is one client-side snippet with no backend. Fund crew and licences against a booked production slate. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a antique dealer website","u":"/recipes/quickstart-embed-finance-option-for-antique-dealers/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a antique dealer website takes one client-side snippet and no backend. Fund stock purchases at auction without tying up working capital. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet. Pre-set a sensible amount for the sector and let the reader adjust:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;"},{"t":"Quickstart: add a Credicorp finance option to a appliance repair firm website","u":"/recipes/quickstart-embed-finance-option-for-appliance-repair/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a appliance repair firm website takes one client-side snippet and no backend. Stock parts and fund a second van as jobs grow. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet. Pre-set a sensible amount for the sector and let the reader adjust:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;"},{"t":"Quickstart: add a Credicorp finance option to a architecture practice website","u":"/recipes/quickstart-embed-finance-option-for-architects/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a architecture practice website takes one client-side snippet and no backend. Bridge project milestones and hire ahead of a won pitch. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet. Pre-set a sensible amount for the sector and let the reader adjust:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;"},{"t":"Quickstart: add a Credicorp finance option to a bakery website","u":"/recipes/quickstart-embed-finance-option-for-bakeries/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a bakery website takes one client-side snippet and no backend. Spread the cost of an oven, a second site or a seasonal stock build. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet. Pre-set a sensible amount for the sector and let the reader adjust:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;"},{"t":"Quickstart: add a Credicorp finance option to a barbershop website","u":"/recipes/quickstart-embed-finance-option-for-barbershops/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a barbershop website takes one client-side snippet and no backend. Fit out a new chair, a second location or a booking system. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet. Pre-set a sensible amount for the sector and let the reader adjust:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;"},{"t":"Quickstart: add a Credicorp finance option to a beauty salon website","u":"/recipes/quickstart-embed-finance-option-for-beauty-salons/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a beauty salon website takes one client-side snippet and no backend. Fund equipment, refits and stock for a busy season. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet. Pre-set a sensible amount for the sector and let the reader adjust:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;"},{"t":"Quickstart: add a Credicorp finance option to a bike shop website","u":"/recipes/quickstart-embed-finance-option-for-bike-shops/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a bike shop website takes one client-side snippet and no backend. Build seasonal stock and fund a workshop fit-out. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet. Pre-set a sensible amount for the sector and let the reader adjust:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;"},{"t":"Quickstart: add a Credicorp finance option to a boatyard website","u":"/recipes/quickstart-embed-finance-option-for-boatyards-marine-services/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a boatyard website takes one client-side snippet and no backend. Fund a haul-out season's materials and labour up front. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet. Pre-set a sensible amount for the sector and let the reader adjust:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;"},{"t":"Quickstart: add a Credicorp finance option to a bookkeeping practice website","u":"/recipes/quickstart-embed-finance-option-for-bookkeepers/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a bookkeeping practice website takes one client-side snippet and no backend. Invest in software and staff ahead of client growth. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet. Pre-set a sensible amount for the sector and let the reader adjust:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;"},{"t":"Quickstart: add a Credicorp finance option to a branding agency website","u":"/recipes/quickstart-embed-finance-option-for-branding-agency/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a branding agency website is one client-side snippet with no backend. Hire ahead of a won pitch and smooth project-based income. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a brewery website","u":"/recipes/quickstart-embed-finance-option-for-breweries/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a brewery website takes one client-side snippet and no backend. Fund tanks, kegs and a duty deferment buffer without tying up cash. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet. Pre-set a sensible amount for the sector and let the reader adjust:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;"},{"t":"Quickstart: add a Credicorp finance option to a butcher website","u":"/recipes/quickstart-embed-finance-option-for-butcher/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a butcher website is one client-side snippet with no backend. Fund refrigeration, stock and a shop refit against steady local demand. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a commercial caterer website","u":"/recipes/quickstart-embed-finance-option-for-commercial-caterer/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a commercial caterer website is one client-side snippet with no backend. Fund equipment and deposits ahead of the event season. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a commercial photography studio website","u":"/recipes/quickstart-embed-finance-option-for-commercial-photography-studio/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a commercial photography studio website is one client-side snippet with no backend. Fund kit and studio space against booked commercial shoots. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a construction firm website","u":"/recipes/quickstart-embed-finance-option-for-builders/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a construction firm website takes one client-side snippet and no backend. Fund materials and payroll while you wait on stage payments and retentions. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet. Pre-set a sensible amount for the sector and let the reader adjust:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;"},{"t":"Quickstart: add a Credicorp finance option to a copywriting studio website","u":"/recipes/quickstart-embed-finance-option-for-copywriting-studio/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a copywriting studio website is one client-side snippet with no backend. Hire ahead of a retainer and smooth uneven project income. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a dental laboratory website","u":"/recipes/quickstart-embed-finance-option-for-dental-laboratory/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a dental laboratory website is one client-side snippet with no backend. Fund milling equipment and materials against booked lab work. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a design studio website","u":"/recipes/quickstart-embed-finance-option-for-design-studio/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a design studio website is one client-side snippet with no backend. Invest in kit and a hire against booked design work. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a event hire company website","u":"/recipes/quickstart-embed-finance-option-for-event-hire-company/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a event hire company website is one client-side snippet with no backend. Build stock ahead of the event season and smooth deposit timing. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a exhibition stand builder website","u":"/recipes/quickstart-embed-finance-option-for-exhibition-stand-builder/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a exhibition stand builder website is one client-side snippet with no backend. Fund materials and labour up front while you wait on event payments. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a florist website","u":"/recipes/quickstart-embed-finance-option-for-florist/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a florist website is one client-side snippet with no backend. Fund seasonal stock ahead of peak dates like Valentine's and Mother's Day. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a health and safety consultant website","u":"/recipes/quickstart-embed-finance-option-for-health-and-safety-consultant/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a health and safety consultant website is one client-side snippet with no backend. Fund certification and a hire ahead of contract growth. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a physiotherapy clinic website","u":"/recipes/quickstart-embed-finance-option-for-physiotherapy-clinic/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a physiotherapy clinic website is one client-side snippet with no backend. Fund equipment and a fit-out against a growing patient list. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a picture framer website","u":"/recipes/quickstart-embed-finance-option-for-picture-framer/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a picture framer website is one client-side snippet with no backend. Fund a workshop fit-out and stock against bespoke commissions. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a print finisher website","u":"/recipes/quickstart-embed-finance-option-for-print-finisher/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a print finisher website is one client-side snippet with no backend. Fund finishing kit and stock against contract print volumes. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a private medical clinic website","u":"/recipes/quickstart-embed-finance-option-for-private-medical-clinic/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a private medical clinic website is one client-side snippet with no backend. Spread the cost of devices and consumables against booked demand. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a recording studio website","u":"/recipes/quickstart-embed-finance-option-for-recording-studio/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a recording studio website is one client-side snippet with no backend. Fund equipment and a room build against booked studio time. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a sign and display firm website","u":"/recipes/quickstart-embed-finance-option-for-sign-and-display-firm/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a sign and display firm website is one client-side snippet with no backend. Fund a printer and materials against booked fabrication work. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a specialty coffee roaster website","u":"/recipes/quickstart-embed-finance-option-for-specialty-coffee-roaster/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a specialty coffee roaster website is one client-side snippet with no backend. Fund a roaster and green-bean stock ahead of wholesale growth. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a training provider website","u":"/recipes/quickstart-embed-finance-option-for-training-provider/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a training provider website is one client-side snippet with no backend. Fund course development and delivery ahead of cohort intakes. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a video production company website","u":"/recipes/quickstart-embed-finance-option-for-video-production-company/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a video production company website is one client-side snippet with no backend. Fund equipment and crew against booked production work. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a virtual assistant agency website","u":"/recipes/quickstart-embed-finance-option-for-virtual-assistant-agency/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a virtual assistant agency website is one client-side snippet with no backend. Fund a hire and tooling ahead of a signed client contract. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add a Credicorp finance option to a web design agency website","u":"/recipes/quickstart-embed-finance-option-for-web-design-agency/","c":"Integration recipes","e":"Quickstart","s":"Adding a Credicorp funding option to a web design agency website is one client-side snippet with no backend. Hire ahead of a signed retainer and smooth project-based income. This quickstart drops in a sector-appropriate quote and apply call-to-action using only the unauthenticated public ring.","b":"Drop in the option Because the product list and quote are public, the whole funding block is a self-contained snippet:&lt;div id=&quot;cc-finance&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const res = await fetch(base + &#x27;/quote&#x27;, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; }, body: JSON.stringify({ amount: 25000, term_months: 6 }), }); const q = await res.json(); document.getElementById(&#x27;cc-finance&#x27;).innerHTML = `&lt;p&gt;Borrow \\u00a325,000 from \\u0"},{"t":"Quickstart: add timeouts and retries to a Node.js API client","u":"/recipes/quickstart-add-timeouts-and-retries-node/","c":"Integration recipes","e":"Quickstart","s":"A production API client needs a timeout and a retry policy. In Node, an AbortController bounds every request and a small retry wrapper handles transient 429/5xx — combined with an idempotency key, this turns a naive fetch into a resilient client.","b":"Timeout every call async function fetchWithTimeout(url, init = {}, ms = 8000) { const ac = new AbortController(); const t = setTimeout(() =&gt; ac.abort(), ms); try { return await fetch(url, { ...init, signal: ac.signal }); } finally { clearTimeout(t); } } Retry only what's safe async function resilient(url, init) { for (let attempt = 0; attempt &lt; 4; attempt++) { try { const res = await fetchWithTimeout(url, init); if (res.status &lt; 500 &amp;&amp; res.status !== 429) return res; } catch (e) { if (e.name !== &#x27;AbortError&#x27; &amp;&amp; attempt === 3) throw e; } await new Promise(r =&"},{"t":"Quickstart: call the Credicorp public API from Bun","u":"/recipes/quickstart-bun-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic Bun way to make your first Credicorp public API call. List the live products from GET /public/v1/products, set a timeout, and turn the documented error envelope into an error you can handle — the pattern every other public-ring call reuses.","b":"List products const res = await fetch( &#x27;https://api.credicorp.co.uk/public/v1/products&#x27;, { headers: { Accept: &#x27;application/json&#x27; } }, ); if (!res.ok) throw new Error(`${res.status}`); const { data } = await res.json(); console.log(data);Bun implements the standard fetch, so the Node examples run unchanged and start faster. Read the base URL from Bun.env or process.env. Use the sandbox in development Point the base host at https://sandbox.credicorp.co.uk/public/v1 in development and CI, and at https://api.credicorp.co.uk/public/v1 in production, driven by one environment var"},{"t":"Quickstart: call the Credicorp public API from C# / .NET","u":"/recipes/quickstart-csharp-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic C# / .NET way to make your first Credicorp public API call. List the live products from GET /public/v1/products, set a timeout, and turn the documented error envelope into an error you can handle — the pattern every other public-ring call reuses.","b":"List products using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; http.DefaultRequestHeaders.Accept.Add(new(&quot;application/json&quot;)); var res = await http.GetAsync(&quot;https://api.credicorp.co.uk/public/v1/products&quot;); res.EnsureSuccessStatusCode(); var json = await res.Content.ReadFromJsonAsync&lt;ProductList&gt;();Use a single shared HttpClient (never one per call) and ReadFromJsonAsync with a typed record. Register it via IHttpClientFactory in ASP.NET Core for pooling and resilience policies. Use the sandbox in development Point the base host at https://sandb"},{"t":"Quickstart: call the Credicorp public API from Dart / Flutter","u":"/recipes/quickstart-dart-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic Dart / Flutter way to make your first Credicorp public API call. List the live products from GET /public/v1/products, set a timeout, and turn the documented error envelope into an error you can handle — the pattern every other public-ring call reuses.","b":"List products import &#x27;package:http/http.dart&#x27; as http; import &#x27;dart:convert&#x27;; final res = await http.get( Uri.parse(&#x27;https://api.credicorp.co.uk/public/v1/products&#x27;), headers: {&#x27;Accept&#x27;: &#x27;application/json&#x27;}, ).timeout(const Duration(seconds: 10)); if (res.statusCode &gt;= 400) throw Exception(res.body); final data = jsonDecode(res.body)[&#x27;data&#x27;];In Flutter, use the http package and decode with dart:convert. Fetch off the UI isolate and open the enquiry handoff with url_launcher in an in-app web view. Use the sandbox in development Poin"},{"t":"Quickstart: call the Credicorp public API from Deno","u":"/recipes/quickstart-deno-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic Deno way to make your first Credicorp public API call. List the live products from GET /public/v1/products, set a timeout, and turn the documented error envelope into an error you can handle — the pattern every other public-ring call reuses.","b":"List products const res = await fetch( &#x27;https://api.credicorp.co.uk/public/v1/products&#x27;, { headers: { Accept: &#x27;application/json&#x27; } }, ); if (!res.ok) throw new Error(String(res.status)); const { data } = await res.json(); console.log(data);Deno's fetch is Web-standard and needs no imports. Run with --allow-net=api.credicorp.co.uk so the permission is scoped to exactly the host you call. Use the sandbox in development Point the base host at https://sandbox.credicorp.co.uk/public/v1 in development and CI, and at https://api.credicorp.co.uk/public/v1 in production, driven by o"},{"t":"Quickstart: call the Credicorp public API from Elixir","u":"/recipes/quickstart-elixir-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic Elixir way to make your first Credicorp public API call. List the live products from GET /public/v1/products, set a timeout, and turn the documented error envelope into an error you can handle — the pattern every other public-ring call reuses.","b":"List products {:ok, %{status: status, body: body}} = Req.get(&quot;https://api.credicorp.co.uk/public/v1/products&quot;, headers: [{&quot;accept&quot;, &quot;application/json&quot;}], receive_timeout: 10_000) if status &gt;= 400, do: raise(body[&quot;error&quot;][&quot;code&quot;]) products = body[&quot;data&quot;]Use Req (or Finch/Tesla) — it decodes JSON automatically. In a Phoenix app, wrap the call in a context module and cache the catalogue in ETS or Cachex. Use the sandbox in development Point the base host at https://sandbox.credicorp.co.uk/public/v1 in development and CI, and at https:"},{"t":"Quickstart: call the Credicorp public API from Go","u":"/recipes/quickstart-go-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"Go's standard library is all you need to call the Credicorp public API. This quickstart uses net/http and encoding/json to list products into a typed struct, with a context timeout and decoding of the documented error envelope — no third-party HTTP client required.","b":"List products Define a struct that matches the envelope, then decode into it:package main import ( &quot;context&quot; &quot;encoding/json&quot; &quot;fmt&quot; &quot;net/http&quot; &quot;time&quot; ) type Product struct { ID string `json:&quot;id&quot;` Name string `json:&quot;name&quot;` MaxAmount float64 `json:&quot;max_amount&quot;` } type ListResp struct { Data []Product `json:&quot;data&quot;` Error *struct { Code string `json:&quot;code&quot;` Message string `json:&quot;message&quot;` } `json:&quot;error&quot;` } func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*"},{"t":"Quickstart: call the Credicorp public API from Java","u":"/recipes/quickstart-java-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic Java way to make your first Credicorp public API call. List the live products from GET /public/v1/products, set a timeout, and turn the documented error envelope into an error you can handle — the pattern every other public-ring call reuses.","b":"List products import java.net.http.*; import java.net.URI; var client = HttpClient.newHttpClient(); var req = HttpRequest.newBuilder() .uri(URI.create(&quot;https://api.credicorp.co.uk/public/v1/products&quot;)) .header(&quot;Accept&quot;, &quot;application/json&quot;) .timeout(java.time.Duration.ofSeconds(10)) .build(); var res = client.send(req, HttpResponse.BodyHandlers.ofString()); if (res.statusCode() &gt;= 400) throw new RuntimeException(res.body()); System.out.println(res.body());Java 11+ ships java.net.http.HttpClient, so you can call the API with no third-party HTTP library. Decode th"},{"t":"Quickstart: call the Credicorp public API from Kotlin","u":"/recipes/quickstart-kotlin-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic Kotlin way to make your first Credicorp public API call. List the live products from GET /public/v1/products, set a timeout, and turn the documented error envelope into an error you can handle — the pattern every other public-ring call reuses.","b":"List products val client = OkHttpClient.Builder() .callTimeout(10, TimeUnit.SECONDS) .build() val req = Request.Builder() .url(&quot;https://api.credicorp.co.uk/public/v1/products&quot;) .header(&quot;Accept&quot;, &quot;application/json&quot;) .build() client.newCall(req).execute().use { res -&gt; if (!res.isSuccessful) error(res.code) println(res.body?.string()) }On the JVM, OkHttp is the idiomatic choice; on Android it is already common. Reuse one OkHttpClient, set a call timeout, and parse with kotlinx.serialization or Moshi. Use the sandbox in development Point the base host at https://sa"},{"t":"Quickstart: call the Credicorp public API from Node.js","u":"/recipes/quickstart-node-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"Node 18+ ships a global fetch, so you can call the Credicorp public API with zero dependencies. This quickstart lists the live products from GET /public/v1/products, parses the JSON envelope and handles the error shape — the pattern every other public-ring call reuses.","b":"List products With a modern Node runtime you do not need axios or node-fetch — the global fetch is enough:const BASE = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; async function listProducts() { const res = await fetch(`${BASE}/products`, { headers: { Accept: &#x27;application/json&#x27; }, }); if (!res.ok) { const err = await res.json(); throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`); } const { data } = await res.json(); return data; } listProducts().then(products =&gt; { for (const p of products) { console.log(`${p.name} — up to £${p.max_amount.toLocaleStri"},{"t":"Quickstart: call the Credicorp public API from PHP","u":"/recipes/quickstart-php-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"You can call the Credicorp public API from PHP with the built-in cURL extension and no Composer packages. This quickstart performs GET /public/v1/products, decodes the JSON envelope into an array and shows the minimal error handling you need before wiring the response into a page or a Laravel controller.","b":"List products A single cURL handle is enough for a read-only public call:&lt;?php $base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; $ch = curl_init($base . &#x27;/products&#x27;); curl_setopt_array($ch, [ CURLOPT_RETURNURTRANSFER =&gt; true, CURLOPT_HTTPHEADER =&gt; [&#x27;Accept: application/json&#x27;], CURLOPT_TIMEOUT =&gt; 10, ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); $payload = json_decode($body, true); if ($status &gt;= 400) { $e = $payload[&#x27;error&#x27;] ?? [&#x27;code&#x27; =&gt; &#x27;unknown&#x27;, &#x27;message&#x2"},{"t":"Quickstart: call the Credicorp public API from Perl","u":"/recipes/quickstart-perl-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic Perl way to make your first Credicorp public API call. List the live products from GET /public/v1/products, set a timeout, and turn the documented error envelope into an error you can handle — the pattern every other public-ring call reuses.","b":"List products use HTTP::Tiny; use JSON::PP; my $res = HTTP::Tiny-&gt;new(timeout =&gt; 10)-&gt;get( &#x27;https://api.credicorp.co.uk/public/v1/products&#x27;, { headers =&gt; { &#x27;Accept&#x27; =&gt; &#x27;application/json&#x27; } }); die $res-&gt;{status} unless $res-&gt;{success}; my $data = decode_json($res-&gt;{content})-&gt;{data};HTTP::Tiny ships with modern Perl and needs no CPAN install for a simple GET. Decode with JSON::PP (also core) and check the success flag before using the body. Use the sandbox in development Point the base host at https://sandbox.credicorp.co.uk/public/v1 in"},{"t":"Quickstart: call the Credicorp public API from PowerShell","u":"/recipes/quickstart-powershell-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic PowerShell way to make your first Credicorp public API call. List the live products from GET /public/v1/products, set a timeout, and turn the documented error envelope into an error you can handle — the pattern every other public-ring call reuses.","b":"List products $res = Invoke-RestMethod ` -Uri &#x27;https://api.credicorp.co.uk/public/v1/products&#x27; ` -Headers @{ Accept = &#x27;application/json&#x27; } ` -TimeoutSec 10 $res.data | ForEach-Object { &quot;$($_.name): up to $($_.max_amount)&quot; }Invoke-RestMethod parses JSON into objects automatically, so you can pipe straight into ForEach-Object. Use it for Windows automation and cross-platform PowerShell scripts. Use the sandbox in development Point the base host at https://sandbox.credicorp.co.uk/public/v1 in development and CI, and at https://api.credicorp.co.uk/public/v1 in product"},{"t":"Quickstart: call the Credicorp public API from Python","u":"/recipes/quickstart-python-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"With the requests library you can call the Credicorp public API in about ten lines of Python. This quickstart lists the live products from GET /public/v1/products, unwraps the data envelope and turns the documented error shape into a Python exception you can catch.","b":"List products The whole call, including error handling, fits on a screen:import requests BASE = &#x27;https://api.credicorp.co.uk/public/v1&#x27; def list_products(): r = requests.get(f&#x27;{BASE}/products&#x27;, headers={&#x27;Accept&#x27;: &#x27;application/json&#x27;}, timeout=10) body = r.json() if r.status_code &gt;= 400: err = body.get(&#x27;error&#x27;, {}) raise RuntimeError(f&quot;{r.status_code} {err.get(&#x27;code&#x27;)}: {err.get(&#x27;message&#x27;)}&quot;) return body[&#x27;data&#x27;] for product in list_products(): print(f&quot;{product[&#x27;name&#x27;]} \\u2014 up to \\u00a3{"},{"t":"Quickstart: call the Credicorp public API from Ruby","u":"/recipes/quickstart-ruby-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"Ruby's standard library calls the Credicorp public API without a gem. This quickstart uses net/http and json to list products, parse the envelope and raise on the documented error shape — drop it into a Rails service object or a plain script.","b":"List products require &#x27;net/http&#x27; require &#x27;json&#x27; require &#x27;uri&#x27; BASE = &#x27;https://api.credicorp.co.uk/public/v1&#x27; def list_products uri = URI(&quot;#{BASE}/products&quot;) res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 10) do |http| http.get(uri.request_uri, &#x27;Accept&#x27; =&gt; &#x27;application/json&#x27;) end body = JSON.parse(res.body, symbolize_names: true) if res.code.to_i &gt;= 400 e = body[:error] || {} raise &quot;#{res.code} #{e[:code]}: #{e[:message]}&quot; end body[:data] end list_products.each do |p| puts &quot;#{p[:na"},{"t":"Quickstart: call the Credicorp public API from Rust","u":"/recipes/quickstart-rust-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic Rust way to make your first Credicorp public API call. List the live products from GET /public/v1/products, set a timeout, and turn the documented error envelope into an error you can handle — the pattern every other public-ring call reuses.","b":"List products use reqwest::Client; let client = Client::builder() .timeout(std::time::Duration::from_secs(10)) .build()?; let res = client .get(&quot;https://api.credicorp.co.uk/public/v1/products&quot;) .header(&quot;Accept&quot;, &quot;application/json&quot;) .send().await? .error_for_status()?; let body: serde_json::Value = res.json().await?;Use reqwest with tokio, build one Client and reuse it, and let error_for_status() turn a non-2xx into an error. Model the response with serde structs for full type safety. Use the sandbox in development Point the base host at https://sandbox.credicorp.c"},{"t":"Quickstart: call the Credicorp public API from Swift","u":"/recipes/quickstart-swift-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic Swift way to make your first Credicorp public API call. List the live products from GET /public/v1/products, set a timeout, and turn the documented error envelope into an error you can handle — the pattern every other public-ring call reuses.","b":"List products var req = URLRequest(url: URL(string: &quot;https://api.credicorp.co.uk/public/v1/products&quot;)!) req.setValue(&quot;application/json&quot;, forHTTPHeaderField: &quot;Accept&quot;) req.timeoutInterval = 10 let (data, resp) = try await URLSession.shared.data(for: req) guard (resp as? HTTPURLResponse)?.statusCode ?? 500 &lt; 400 else { throw ApiError.badStatus } let list = try JSONDecoder().decode(ProductList.self, from: data)Use URLSession with async/await and a Decodable model. On iOS, run the request off the main actor and open the enquiry handoff in an SFSafariViewController."},{"t":"Quickstart: call the Credicorp public API from shell + jq","u":"/recipes/quickstart-shell-jq-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic shell + jq way to make your first Credicorp public API call. List the live products from GET /public/v1/products, set a timeout, and turn the documented error envelope into an error you can handle — the pattern every other public-ring call reuses.","b":"List products curl -s https://api.credicorp.co.uk/public/v1/products \\ -H &#x27;Accept: application/json&#x27; \\ | jq -r &#x27;.data[] | &quot;\\(.name): up to \\(.max_amount)&quot;&#x27;For scripts and CI, pipe curl into jq to extract exactly the fields you need. Add --fail-with-body to curl so a non-2xx exits non-zero and surfaces the error envelope. Use the sandbox in development Point the base host at https://sandbox.credicorp.co.uk/public/v1 in development and CI, and at https://api.credicorp.co.uk/public/v1 in production, driven by one environment variable. See choosing a base URL. Next st"},{"t":"Quickstart: call the public API from the browser safely","u":"/recipes/quickstart-call-public-api-from-browser/","c":"Integration recipes","e":"Quickstart","s":"The public ring sends permissive CORS headers for read endpoints, so you can call it straight from the browser. That is perfect for a client-side product picker or calculator — but partner keys and any write that matters belong on your server, never in code the user can view.","b":"Fetch from the page // Runs safely in the browser — read-only, no secrets const res = await fetch(&#x27;https://api.credicorp.co.uk/public/v1/products&#x27;); const { data } = await res.json(); renderProductPicker(data);Because these endpoints need no auth, there is nothing secret to leak. Product data, pricing and quotes are all safe to fetch client-side. The line you must not cross The partner ring uses OAuth2 client secrets. If a secret reaches the browser it is compromised the moment the page loads. Proxy any authenticated call through your own backend and keep credentials server-side. Thi"},{"t":"Quickstart: choose the right base URL — sandbox vs production","u":"/recipes/quickstart-choose-base-url-sandbox-vs-production/","c":"Integration recipes","e":"Quickstart","s":"Every Credicorp API integration should read its base host from configuration, never hard-code it. Development and CI point at the sandbox mirror; production points at the live host. Getting this one variable right is what keeps test enquiries out of your live pipeline.","b":"The two hosts Production — https://api.credicorp.co.uk. Real product data, real enquiries, production rate limits.Sandbox — https://sandbox.credicorp.co.uk. A deterministic mirror with test data, looser limits and no side effects; safe to hammer in CI.Both expose the identical /public/v1 route tree, so the only difference between environments is the host. Drive it from one variable # .env.development CREDICORP_BASE=https://sandbox.credicorp.co.uk/public/v1 # .env.production CREDICORP_BASE=https://api.credicorp.co.uk/public/v1Read CREDICORP_BASE everywhere and you will never accidentally submit"},{"t":"Quickstart: connect an AI agent to the Credicorp MCP server","u":"/recipes/quickstart-connect-mcp-server/","c":"Integration recipes","e":"Quickstart","s":"POST /public/v1/mcp is a Model Context Protocol endpoint that exposes the public API as agent tools. Point an MCP-capable assistant at it and the agent can list products, read pricing and request quotes through structured tool calls — no bespoke integration code on the agent side.","b":"What the server exposes The MCP endpoint advertises a tool set mapped onto the public ring — for example list_products, get_pricing and quote_loan. An agent discovers them via the standard MCP handshake and calls them like any other tool. Point a client at it Any MCP client can connect. The transport is JSON-RPC over HTTP POST to the endpoint:{ &quot;jsonrpc&quot;: &quot;2.0&quot;, &quot;id&quot;: 1, &quot;method&quot;: &quot;tools/list&quot;, &quot;params&quot;: {} }The server responds with the available tools and their JSON schemas; the agent then issues tools/call requests to run them. Keep"},{"t":"Quickstart: embed a product picker with the public API","u":"/recipes/quickstart-embed-product-picker-widget/","c":"Integration recipes","e":"Quickstart","s":"A product picker is the simplest high-value embed: fetch the catalogue, render options, link to apply. Because the product list needs no auth, the whole widget is a self-contained snippet you can drop into any page, with every option linking to the real application flow.","b":"The embeddable snippet &lt;div id=&quot;cc-picker&quot;&gt;&lt;/div&gt; &lt;script&gt; (async () =&gt; { const base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; const { data } = await (await fetch(base + &#x27;/products&#x27;)).json(); const el = document.getElementById(&#x27;cc-picker&#x27;); el.innerHTML = data.map(p =&gt; `&lt;a class=&quot;cc-card&quot; href=&quot;https://clients.credicorp.co.uk/register?product=${p.id}&quot;&gt;` + `&lt;strong&gt;${p.name}&lt;/strong&gt;` + `&lt;span&gt;\\u00a3${p.min_amount.toLocaleString()}\\u2013\\u00a3${p.max_amount.toLocaleString()}&lt;/span&gt;"},{"t":"Quickstart: fetch a single Credicorp product by id","u":"/recipes/quickstart-get-a-single-product/","c":"Integration recipes","e":"Quickstart","s":"GET /public/v1/products/{id} returns the full detail for one product. Use it to power a product page after the reader picks an item from the list — it carries the same headline fields plus any per-product term options and eligibility notes.","b":"Fetch one product curl -s https://api.credicorp.co.uk/public/v1/products/flex-business \\ -H &#x27;Accept: application/json&#x27;The id is the stable string from the list response (for example flex-business). Passing an unknown id returns a 404 with code: not_found. Handle the not-found case r = requests.get(f&#x27;{BASE}/products/{pid}&#x27;, timeout=10) if r.status_code == 404: return render_unknown_product(pid) r.raise_for_status() product = r.json() Cache per product Product detail is as cacheable as the list. Cache by id and honour the response Cache-Control; invalidate when your list-leve"},{"t":"Quickstart: fetch published CMS content by key","u":"/recipes/quickstart-fetch-cms-page/","c":"Integration recipes","e":"Quickstart","s":"GET /public/v1/cms/pages/{key} serves published Credicorp content by a stable key. Use it to pull canonical copy — a legal notice, a product blurb, a disclosure — into your own page so it stays in sync with the source of truth instead of being copied and left to rot.","b":"Fetch a page by key curl -s https://api.credicorp.co.uk/public/v1/cms/pages/representative-example \\ -H &#x27;Accept: application/json&#x27;The response carries the content title, the body (as sanitised HTML or Markdown) and a updated_at timestamp. Cache it and refresh when updated_at changes so your copy is never stale. Why pull instead of copy Regulated copy — a representative example, a risk warning — must match the source exactly. Fetching it by key means a change on our side propagates to your integration automatically, with no manual re-paste and no drift. What keys are available? Keys a"},{"t":"Quickstart: fetch the support widget configuration","u":"/recipes/quickstart-fetch-support-widget-config/","c":"Integration recipes","e":"Quickstart","s":"GET /public/v1/support/widget returns the configuration for the embeddable Credicorp support widget. Read the branding, entry points and available channels so you can render a native-feeling help launcher in your own UI, then hand conversations to the chat endpoint.","b":"Fetch the config curl -s https://api.credicorp.co.uk/public/v1/support/widget -H &#x27;Accept: application/json&#x27;The response describes the widget's branding, greeting and the channels it offers (chat, links to help articles). Use it to render a launcher that matches your host page rather than an iframe you cannot style. Hand off to chat Once the user opens your launcher, send their messages to the support chat endpoint and stream the replies back. The widget config and the chat endpoint together let you build a fully custom support surface. Do I have to use the prebuilt widget? No. This e"},{"t":"Quickstart: get an indicative loan quote from the public API","u":"/recipes/quickstart-quote-a-loan-public-api/","c":"Integration recipes","e":"Quickstart","s":"POST /public/v1/quote turns an amount and term into an illustrative repayment. Send the requested amount and term in months and read back the periodic repayment, total repayable and representative APR — everything you need for a self-serve cost illustration before the reader applies.","b":"Request a quote curl -s -X POST https://api.credicorp.co.uk/public/v1/quote \\ -H &#x27;Content-Type: application/json&#x27; \\ -H &#x27;Accept: application/json&#x27; \\ -d &#x27;{&quot;amount&quot;: 25000, &quot;term_months&quot;: 6, &quot;product&quot;: &quot;flex-business&quot;}&#x27;The response carries the periodic repayment, the total_repayable and the representative_apr used. Amounts outside the product's range return a 400 with a field-level validation error. Show it to the reader const res = await fetch(`${BASE}/quote`, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#"},{"t":"Quickstart: handle Credicorp API error responses","u":"/recipes/quickstart-handle-error-responses/","c":"Integration recipes","e":"Quickstart","s":"Every Credicorp API error uses the same envelope: { error: { type, code, message, request_id } }. Branch on the stable code, log the request_id, and show the reader a friendly message — one handler covers 400 through 503 across every endpoint.","b":"The envelope { &quot;error&quot;: { &quot;type&quot;: &quot;validation&quot;, &quot;code&quot;: &quot;invalid_amount&quot;, &quot;message&quot;: &quot;amount must be between 1000 and 150000&quot;, &quot;request_id&quot;: &quot;req_44be1c&quot; } }type is a broad family (validation, rate_limit, auth, not_found, server). code is a stable machine string. message is human-readable and may change — never branch on it. One handler for all of them async function call(url, init) { const res = await fetch(url, init); const body = await res.json().catch(() =&gt; ({})); if (!res.ok) { const e = body.erro"},{"t":"Quickstart: handle rate limits on the public API","u":"/recipes/quickstart-handle-rate-limits-public-api/","c":"Integration recipes","e":"Quickstart","s":"The public ring is rate-limited, and a 429 tells you exactly when to try again. Read the RateLimit-Remaining and RateLimit-Reset headers to stay ahead of the limit, and on a 429 back off with jitter until the window resets — the difference between a resilient client and a hammered one.","b":"Read the headers Every response carries the current budget so you can slow down before you are cut off:RateLimit-Limit — requests allowed per window.RateLimit-Remaining — how many you have left.RateLimit-Reset — seconds until the window resets. Back off on 429 async function withRetry(fn, max = 4) { for (let attempt = 0; ; attempt++) { const res = await fn(); if (res.status !== 429) return res; if (attempt &gt;= max) return res; const reset = Number(res.headers.get(&#x27;RateLimit-Reset&#x27;)) || 2 ** attempt; const jitter = Math.random() * 250; await new Promise(r =&gt; setTimeout(r, reset *"},{"t":"Quickstart: list Credicorp business-finance products","u":"/recipes/quickstart-list-business-finance-products/","c":"Integration recipes","e":"Quickstart","s":"GET /public/v1/products returns the live catalogue of Credicorp business-finance products. Each item carries the amount range, term, representative APR and product type, so one call gives you everything you need to render a comparison table or a product picker without any authentication.","b":"Fetch the catalogue curl -s https://api.credicorp.co.uk/public/v1/products -H &#x27;Accept: application/json&#x27;The response is a data array. Read min_amount/max_amount for the lending range, type to tell flex from onetime, and representative_apr for the headline cost figure. Render a comparison row Map each product straight into your UI. The example builds a simple table row per product:const { data } = await (await fetch(`${BASE}/products`)).json(); const rows = data.map(p =&gt; `&lt;tr&gt;&lt;td&gt;${p.name}&lt;/td&gt;` + `&lt;td&gt;\\u00a3${p.min_amount.toLocaleString()}\\u2013\\u00a3${p.ma"},{"t":"Quickstart: list Credicorp loyalty tiers","u":"/recipes/quickstart-list-loyalty-tiers/","c":"Integration recipes","e":"Quickstart","s":"GET /public/v1/loyalty/tiers returns the public loyalty ladder. Each tier carries a name, the qualifying threshold and its benefits, so you can render a rewards ladder or explain to a prospective borrower how on-time repayment and tenure are recognised.","b":"Fetch the ladder curl -s https://api.credicorp.co.uk/public/v1/loyalty/tiers -H &#x27;Accept: application/json&#x27;Tiers come back ordered by threshold. Render them as a ladder and highlight the benefits that change between adjacent tiers so the progression is obvious. Render the ladder tiers = requests.get(f&#x27;{BASE}/loyalty/tiers&#x27;).json()[&#x27;data&#x27;] for t in tiers: print(f&quot;{t[&#x27;name&#x27;]}: from \\u00a3{t[&#x27;threshold&#x27;]:,} \\u2014 {&#x27;, &#x27;.join(t[&#x27;benefits&#x27;])}&quot;) Is a member's own tier available here? No. This endpoint lists the tier defin"},{"t":"Quickstart: paginate through list results with a cursor","u":"/recipes/quickstart-paginate-list-endpoints/","c":"Integration recipes","e":"Quickstart","s":"Credicorp list endpoints use cursor pagination: follow next_cursor until it is null. Cursor paging is stable under inserts, so you never skip or double-count a row the way offset paging can. This recipe drains a list endpoint completely in a simple loop.","b":"The pagination loop def fetch_all(path): items, cursor = [], None while True: params = {&#x27;limit&#x27;: 100} if cursor: params[&#x27;cursor&#x27;] = cursor body = requests.get(f&#x27;{BASE}/{path}&#x27;, params=params, timeout=10).json() items.extend(body[&#x27;data&#x27;]) cursor = body.get(&#x27;next_cursor&#x27;) if not cursor: return itemsPass the returned next_cursor back as the cursor parameter on the next request. When the response has no next_cursor (or it is null), you have reached the end. Why cursors, not offsets Offset paging (?page=3) breaks when rows are inserted between reque"},{"t":"Quickstart: read live API status programmatically","u":"/recipes/quickstart-read-api-status/","c":"Integration recipes","e":"Quickstart","s":"GET /public/v1/status gives your integration a machine-readable view of API health. Read it to gracefully degrade a non-critical feature when a component is impaired — for example hiding a live quote widget during a partial outage rather than showing the reader an error.","b":"Read the status curl -s https://api.credicorp.co.uk/public/v1/status -H &#x27;Accept: application/json&#x27;The response summarises overall health and lists component states. Branch on the overall indicator to decide whether to show a live-API feature or fall back to cached data. Degrade, don't fail const { status } = await (await fetch(`${BASE}/status`)).json(); if (status !== &#x27;operational&#x27;) { showCachedProducts(); // fall back, don&#x27;t error } else { showLiveQuoteWidget(); } Read the component breakdown Beyond the overall indicator, the response lists individual components so yo"},{"t":"Quickstart: read public pricing configuration","u":"/recipes/quickstart-list-config-pricing/","c":"Integration recipes","e":"Quickstart","s":"GET /public/v1/config/pricing exposes the representative pricing parameters behind the public quote. Use it to show headline rates, fee bands and lending bounds on a pricing page, and to keep your own client-side calculators in step with the figures the API actually uses.","b":"Fetch the pricing config curl -s https://api.credicorp.co.uk/public/v1/config/pricing -H &#x27;Accept: application/json&#x27;You get the representative rate bands, any fixed fees and the min/max amount and term bounds. These are the same parameters the quote endpoint applies, so your on-page figures and the API never drift apart. Feed a client-side calculator Rather than hard-coding rates in your front-end, fetch them once and pass them into your calculator so a pricing change on our side flows through automatically:const cfg = await (await fetch(`${BASE}/config/pricing`)).json(); calculator.s"},{"t":"Quickstart: record marketing and data consent","u":"/recipes/quickstart-capture-consent/","c":"Integration recipes","e":"Quickstart","s":"POST /public/v1/consent records a user's consent choices at the point of collection. Capture the consent alongside an enquiry so every downstream contact respects the user's decision — and you have a timestamped, auditable record of exactly what they agreed to.","b":"Record the choices curl -s -X POST https://api.credicorp.co.uk/public/v1/consent \\ -H &#x27;Content-Type: application/json&#x27; \\ -d &#x27;{ &quot;email&quot;: &quot;a@northgate.example&quot;, &quot;purposes&quot;: { &quot;marketing_email&quot;: true, &quot;marketing_sms&quot;: false }, &quot;source&quot;: &quot;partner-widget&quot; }&#x27;Consent is stored per purpose so you can honour a user who wants product updates but not SMS. The response echoes the stored record with a server timestamp you can log for your own audit trail. Capture it at the source Record consent at the exact moment it "},{"t":"Quickstart: send a message to the support chat endpoint","u":"/recipes/quickstart-post-a-support-chat-message/","c":"Integration recipes","e":"Quickstart","s":"POST /public/v1/support/chat powers a conversational support experience. Post the visitor's message with a conversation id and read back the assistant's reply — enough to build a fully custom chat surface on top of Credicorp's public support brain.","b":"Send a message curl -s -X POST https://api.credicorp.co.uk/public/v1/support/chat \\ -H &#x27;Content-Type: application/json&#x27; \\ -d &#x27;{ &quot;conversation_id&quot;: &quot;conv_8a1c&quot;, &quot;message&quot;: &quot;What is the maximum I can borrow for six months?&quot; }&#x27;The response carries the assistant reply and the conversation_id to reuse on the next turn. Keep sending the same id to preserve context across the conversation. Wire it into your UI async function ask(conversationId, message) { const res = await fetch(`${BASE}/support/chat`, { method: &#x27;POST&#x27;, headers: { "},{"t":"Quickstart: set the right request headers for the public API","u":"/recipes/quickstart-set-request-headers-correctly/","c":"Integration recipes","e":"Quickstart","s":"A handful of request headers make the difference between a clean integration and a flaky one. Always send Accept: application/json, set Content-Type on writes, add an Idempotency-Key to POSTs and identify your client with a descriptive User-Agent so support can trace your traffic.","b":"The four headers that matter Accept: application/json — guarantees a JSON body even on errors.Content-Type: application/json — required on POST/PUT bodies.Idempotency-Key: &lt;uuid&gt; — makes a retried POST safe to send twice (see idempotency).User-Agent: my-app/1.4 (+https://example.com) — lets support correlate your calls. Set them once Centralise headers in one place — a base client, an interceptor or a wrapper function — so every call is consistent:const base = (extra = {}) =&gt; ({ Accept: &#x27;application/json&#x27;, &#x27;User-Agent&#x27;: &#x27;my-app/1.4&#x27;, ...extra, }); // GET "},{"t":"Quickstart: submit a business-finance enquiry","u":"/recipes/quickstart-submit-an-enquiry/","c":"Integration recipes","e":"Quickstart","s":"POST /public/v1/enquiries is how a website or partner hands a lead into Credicorp. Post the company and contact details, and the response returns a handoff_url that takes the applicant straight into the secure application flow — the public ring's one write operation that starts a real journey.","b":"Post the enquiry curl -s -X POST https://api.credicorp.co.uk/public/v1/enquiries \\ -H &#x27;Content-Type: application/json&#x27; \\ -H &#x27;Idempotency-Key: 6f9d2c1a-...&#x27; \\ -d &#x27;{ &quot;company&quot;: { &quot;name&quot;: &quot;Northgate Joinery Ltd&quot;, &quot;number&quot;: &quot;09123456&quot; }, &quot;contact&quot;: { &quot;name&quot;: &quot;A. Director&quot;, &quot;email&quot;: &quot;a@northgate.example&quot; }, &quot;amount&quot;: 30000, &quot;purpose&quot;: &quot;working_capital&quot; }&#x27;A 201 returns the created enquiry's id and a handoff_url. Redirect the applicant there t"},{"t":"Quickstart: use idempotency keys on write requests","u":"/recipes/quickstart-use-idempotency-keys/","c":"Integration recipes","e":"Quickstart","s":"An Idempotency-Key header makes a POST safe to retry. Generate one UUID per logical operation, send it with the write, and reuse the same key on any retry — the API returns the original result instead of creating a second enquiry or consent record.","b":"Generate one key per operation The key identifies a logical action, not a physical request. Generate it once when the user submits, and reuse it if the network fails and you retry:const key = crypto.randomUUID(); async function submit() { return withRetry(() =&gt; fetch(`${BASE}/enquiries`, { method: &#x27;POST&#x27;, headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27;, &#x27;Idempotency-Key&#x27;: key }, body: JSON.stringify(enquiry), })); } What the server does with it On the first request with a given key the API processes it normally and stores the result. A repeat with the s"},{"t":"Quickstart: use the Credicorp public API in AWS Lambda","u":"/recipes/quickstart-integrate-public-api-with-aws-lambda/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from AWS Lambda. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend later for pricing and quotes, always keeping partner secrets server-side.","b":"Call the API from AWS Lambda export const handler = async () =&gt; { const res = await fetch(&#x27;https://api.credicorp.co.uk/public/v1/products&#x27;); const body = await res.json(); return { statusCode: 200, headers: { &#x27;content-type&#x27;: &#x27;application/json&#x27;, &#x27;cache-control&#x27;: &#x27;max-age=3600&#x27; }, body: JSON.stringify(body), }; };In a Lambda (Node 18+ runtime) the global `fetch` calls the API with no bundled dependencies. Keep the base URL in an environment variable and any partner secret in AWS Secrets Manager or an encrypted env var, read at cold start. Poin"},{"t":"Quickstart: use the Credicorp public API in Angular","u":"/recipes/quickstart-integrate-public-api-with-angular/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from Angular. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend for pricing, quotes and enquiries, always keeping partner secrets server-side.","b":"Call the API from Angular @Injectable({ providedIn: &#x27;root&#x27; }) export class CredicorpService { private base = &#x27;https://api.credicorp.co.uk/public/v1&#x27;; constructor(private http: HttpClient) {} products() { return this.http.get&lt;{ data: Product[] }&gt;(`${this.base}/products`) .pipe(map(r =&gt; r.data), shareReplay(1)); } }Wrap the call in an injectable service using HttpClient, and use shareReplay(1) so multiple subscribers share one request. Provide the base URL via an injection token per environment. Sandbox first Set the base URL to https://sandbox.credicorp.co.uk/public"},{"t":"Quickstart: use the Credicorp public API in Astro","u":"/recipes/quickstart-integrate-public-api-with-astro/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from Astro. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend for pricing, quotes and enquiries, always keeping partner secrets server-side.","b":"Call the API from Astro --- // Product.astro (runs at build or on the server) const res = await fetch( &#x27;https://api.credicorp.co.uk/public/v1/products&#x27;); const { data } = await res.json(); --- &lt;ul&gt;{data.map(p =&gt; &lt;li&gt;{p.name}&lt;/li&gt;)}&lt;/ul&gt;In Astro, fetch in the component frontmatter, which runs at build time (SSG) or on the server (SSR). For a static build the catalogue is baked in; rebuild on a schedule to keep it fresh. Sandbox first Set the base URL to https://sandbox.credicorp.co.uk/public/v1 in development and CI. See choosing a base URL. Next steps Add a"},{"t":"Quickstart: use the Credicorp public API in Cloudflare Workers","u":"/recipes/quickstart-integrate-public-api-with-cloudflare-workers/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from Cloudflare Workers. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend later for pricing and quotes, always keeping partner secrets server-side.","b":"Call the API from Cloudflare Workers export default { async fetch(request, env, ctx) { const cache = caches.default; let res = await cache.match(request); if (res) return res; res = await fetch(&#x27;https://api.credicorp.co.uk/public/v1/products&#x27;); res = new Response(res.body, res); res.headers.set(&#x27;Cache-Control&#x27;, &#x27;public, max-age=3600&#x27;); ctx.waitUntil(cache.put(request, res.clone())); return res; }, };A Worker is an ideal cache-and-shape layer for the public catalogue: fetch once, cache at the edge with the Cache API, and serve product data to your front-end with ne"},{"t":"Quickstart: use the Credicorp public API in Django","u":"/recipes/quickstart-integrate-public-api-with-django/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from Django. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend later for pricing and quotes, always keeping partner secrets server-side.","b":"Call the API from Django import requests from django.conf import settings from django.core.cache import cache def products(request): data = cache.get(&#x27;credicorp_products&#x27;) if data is None: r = requests.get(f&#x27;{settings.CREDICORP_BASE}/products&#x27;, timeout=10) r.raise_for_status() data = r.json()[&#x27;data&#x27;] cache.set(&#x27;credicorp_products&#x27;, data, 3600) return render(request, &#x27;products.html&#x27;, {&#x27;products&#x27;: data})Call the API from a view or, better, a service module, and put the base URL in `settings`. Use Django's cache framework to hold the cat"},{"t":"Quickstart: use the Credicorp public API in Express","u":"/recipes/quickstart-integrate-public-api-with-express/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from Express. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend later for pricing and quotes, always keeping partner secrets server-side.","b":"Call the API from Express const express = require(&#x27;express&#x27;); const app = express(); const BASE = process.env.CREDICORP_BASE; app.get(&#x27;/api/products&#x27;, async (req, res) =&gt; { try { const upstream = await fetch(`${BASE}/products`); const body = await upstream.json(); res.set(&#x27;Cache-Control&#x27;, &#x27;public, max-age=3600&#x27;).json(body); } catch (e) { res.status(502).json({ error: &#x27;upstream_unavailable&#x27; }); } });Proxy the public read through your own Express route when you want to add caching, shape the payload, or keep a single origin for your front-end."},{"t":"Quickstart: use the Credicorp public API in FastAPI","u":"/recipes/quickstart-integrate-public-api-with-fastapi/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from FastAPI. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend for pricing, quotes and enquiries, always keeping partner secrets server-side.","b":"Call the API from FastAPI from fastapi import FastAPI import httpx app = FastAPI() BASE = &#x27;https://api.credicorp.co.uk/public/v1&#x27; @app.get(&#x27;/products&#x27;) async def products(): async with httpx.AsyncClient(timeout=10) as client: r = await client.get(f&#x27;{BASE}/products&#x27;) r.raise_for_status() return r.json()[&#x27;data&#x27;]Use an async httpx client inside a FastAPI route and share one client via a lifespan dependency for connection pooling. Cache the catalogue in Redis rather than fetching per request. Sandbox first Set the base URL to https://sandbox.credicorp.co.uk/"},{"t":"Quickstart: use the Credicorp public API in Flask","u":"/recipes/quickstart-integrate-public-api-with-flask/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from Flask. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend for pricing, quotes and enquiries, always keeping partner secrets server-side.","b":"Call the API from Flask from flask import Flask, jsonify import requests app = Flask(__name__) BASE = &#x27;https://api.credicorp.co.uk/public/v1&#x27; @app.get(&#x27;/products&#x27;) def products(): r = requests.get(f&#x27;{BASE}/products&#x27;, timeout=10) r.raise_for_status() return jsonify(r.json()[&#x27;data&#x27;])Call the API from a Flask view with requests, and cache the result with Flask-Caching so the catalogue is not fetched on every request. Keep the base URL in app.config. Sandbox first Set the base URL to https://sandbox.credicorp.co.uk/public/v1 in development and CI. See choosi"},{"t":"Quickstart: use the Credicorp public API in Laravel","u":"/recipes/quickstart-integrate-public-api-with-laravel/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from Laravel. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend later for pricing and quotes, always keeping partner secrets server-side.","b":"Call the API from Laravel use Illuminate\\Support\\Facades\\Http; class CredicorpClient { public function products(): array { return Http::baseUrl(config(&#x27;services.credicorp.base&#x27;)) -&gt;acceptJson() -&gt;timeout(10) -&gt;get(&#x27;products&#x27;) -&gt;throw() -&gt;json(&#x27;data&#x27;); } }Use Laravel's `Http` facade with a base URL from `config/services.php`. `->throw()` turns a non-2xx into an exception carrying the response, and `->timeout()` bounds every call. Cache the result with `Cache::remember` for the product list's max-age. Point at sandbox in development Set the base URL t"},{"t":"Quickstart: use the Credicorp public API in Laravel Livewire","u":"/recipes/quickstart-integrate-public-api-with-laravel-livewire/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from Laravel Livewire. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend for pricing, quotes and enquiries, always keeping partner secrets server-side.","b":"Call the API from Laravel Livewire class FinanceOptions extends Component { public array $products = []; public function mount(): void { $this-&gt;products = cache()-&gt;remember(&#x27;cc_products&#x27;, 3600, fn () =&gt; Http::get(config(&#x27;services.credicorp.base&#x27;).&#x27;/products&#x27;) -&gt;json(&#x27;data&#x27;)); } public function render() { return view(&#x27;livewire.finance-options&#x27;); } }In a Livewire component, fetch and cache the catalogue in mount() and render it in the Blade view. Livewire keeps the interaction server-driven, so no secret ever reaches the browser. Sand"},{"t":"Quickstart: use the Credicorp public API in Next.js","u":"/recipes/quickstart-integrate-public-api-with-nextjs/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from Next.js. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend later for pricing and quotes, always keeping partner secrets server-side.","b":"Call the API from Next.js // app/products/page.tsx (Server Component) export default async function Products() { const res = await fetch(&#x27;https://api.credicorp.co.uk/public/v1/products&#x27;, { next: { revalidate: 3600 }, // cache for an hour }); const { data } = await res.json(); return &lt;ProductGrid products={data} /&gt;; }Fetch in a Server Component so the call happens on your server, out of the browser, and use `next.revalidate` to cache the catalogue. This is also the right place to add partner-ring calls later, since server components keep secrets server-side. Point at sandbox in "},{"t":"Quickstart: use the Credicorp public API in Nuxt","u":"/recipes/quickstart-integrate-public-api-with-nuxt/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from Nuxt. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend for pricing, quotes and enquiries, always keeping partner secrets server-side.","b":"Call the API from Nuxt // composables/useProducts.js export const useProducts = () =&gt; useAsyncData(&#x27;products&#x27;, () =&gt; $fetch(&#x27;https://api.credicorp.co.uk/public/v1/products&#x27;) .then(r =&gt; r.data));Use useAsyncData with Nuxt's $fetch so the call runs on the server and hydrates the client. Cache with Nitro route rules or a server route that proxies and caches. Sandbox first Set the base URL to https://sandbox.credicorp.co.uk/public/v1 in development and CI. See choosing a base URL. Next steps Add a quote form or an enquiry submission, and send applicants to apply. Handl"},{"t":"Quickstart: use the Credicorp public API in React","u":"/recipes/quickstart-integrate-public-api-with-react/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from React. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend later for pricing and quotes, always keeping partner secrets server-side.","b":"Call the API from React import { useEffect, useState } from &#x27;react&#x27;; export function useProducts() { const [products, setProducts] = useState([]); useEffect(() =&gt; { const ac = new AbortController(); fetch(`${import.meta.env.VITE_CREDICORP_BASE}/products`, { signal: ac.signal }) .then(r =&gt; r.json()) .then(({ data }) =&gt; setProducts(data)) .catch(() =&gt; {}); return () =&gt; ac.abort(); }, []); return products; }Wrap the call in a `useEffect` with an `AbortController` so it cancels on unmount. Keep the base URL in `import.meta.env` (Vite) or `process.env` (CRA), and never put "},{"t":"Quickstart: use the Credicorp public API in Remix","u":"/recipes/quickstart-integrate-public-api-with-remix/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from Remix. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend for pricing, quotes and enquiries, always keeping partner secrets server-side.","b":"Call the API from Remix // app/routes/products.jsx export async function loader() { const res = await fetch( &#x27;https://api.credicorp.co.uk/public/v1/products&#x27;); const { data } = await res.json(); return json(data, { headers: { &#x27;Cache-Control&#x27;: &#x27;max-age=3600&#x27; }, }); }Fetch in a Remix loader so the call is server-side, and set a Cache-Control header on the response so the catalogue is cached by the browser and any CDN in front. Sandbox first Set the base URL to https://sandbox.credicorp.co.uk/public/v1 in development and CI. See choosing a base URL. Next steps Add a "},{"t":"Quickstart: use the Credicorp public API in Ruby on Rails","u":"/recipes/quickstart-integrate-public-api-with-rails/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from Ruby on Rails. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend later for pricing and quotes, always keeping partner secrets server-side.","b":"Call the API from Ruby on Rails class CredicorpClient BASE = ENV.fetch(&#x27;CREDICORP_BASE&#x27;) def products Rails.cache.fetch(&#x27;credicorp_products&#x27;, expires_in: 1.hour) do uri = URI(&quot;#{BASE}/products&quot;) res = Net::HTTP.get_response(uri) raise &quot;Credicorp #{res.code}&quot; unless res.code.to_i &lt; 400 JSON.parse(res.body)[&#x27;data&#x27;] end end endPut the call in a service object, read the base URL from `ENV`, and wrap it in `Rails.cache.fetch` so the catalogue is cached for an hour. Keep controllers thin — they should call the service, not the API. Point at sandbo"},{"t":"Quickstart: use the Credicorp public API in Spring Boot","u":"/recipes/quickstart-integrate-public-api-with-spring-boot/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from Spring Boot. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend for pricing, quotes and enquiries, always keeping partner secrets server-side.","b":"Call the API from Spring Boot @Service public class CredicorpClient { private final RestClient client = RestClient.builder() .baseUrl(&quot;https://api.credicorp.co.uk/public/v1&quot;).build(); public List&lt;Product&gt; products() { return client.get().uri(&quot;/products&quot;) .retrieve() .body(ProductList.class).data(); } }Use the modern RestClient (Spring 6.1+) with a configured base URL, map responses to records, and cache with @Cacheable so the catalogue is fetched rarely. Sandbox first Set the base URL to https://sandbox.credicorp.co.uk/public/v1 in development and CI. See choosing a b"},{"t":"Quickstart: use the Credicorp public API in SvelteKit","u":"/recipes/quickstart-integrate-public-api-with-svelte/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from SvelteKit. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend for pricing, quotes and enquiries, always keeping partner secrets server-side.","b":"Call the API from SvelteKit // +page.server.js export async function load({ fetch }) { const res = await fetch( &#x27;https://api.credicorp.co.uk/public/v1/products&#x27;); const { data } = await res.json(); return { products: data }; }Fetch in a SvelteKit load function on the server so the call stays out of the browser, and cache with setHeaders. The returned data is available to your +page.svelte. Sandbox first Set the base URL to https://sandbox.credicorp.co.uk/public/v1 in development and CI. See choosing a base URL. Next steps Add a quote form or an enquiry submission, and send applicants"},{"t":"Quickstart: use the Credicorp public API in Symfony","u":"/recipes/quickstart-integrate-public-api-with-symfony/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from Symfony. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend for pricing, quotes and enquiries, always keeping partner secrets server-side.","b":"Call the API from Symfony use Symfony\\Contracts\\HttpClient\\HttpClientInterface; class CredicorpClient { public function __construct(private HttpClientInterface $http) {} public function products(): array { return $this-&gt;http-&gt;request(&#x27;GET&#x27;, &#x27;https://api.credicorp.co.uk/public/v1/products&#x27;, [&#x27;timeout&#x27; =&gt; 10])-&gt;toArray()[&#x27;data&#x27;]; } }Inject Symfony's HttpClientInterface and call toArray() to decode. Configure a scoped client with the base URI in framework.yaml and cache responses with the Cache component. Sandbox first Set the base URL to https:"},{"t":"Quickstart: use the Credicorp public API in Vue 3","u":"/recipes/quickstart-integrate-public-api-with-vue/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from Vue 3. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend later for pricing and quotes, always keeping partner secrets server-side.","b":"Call the API from Vue 3 &lt;script setup&gt; import { ref, onMounted } from &#x27;vue&#x27;; const products = ref([]); onMounted(async () =&gt; { const res = await fetch(`${import.meta.env.VITE_CREDICORP_BASE}/products`); products.value = (await res.json()).data; }); &lt;/script&gt; &lt;template&gt; &lt;ul&gt;&lt;li v-for=&quot;p in products&quot; :key=&quot;p.id&quot;&gt;{{ p.name }}&lt;/li&gt;&lt;/ul&gt; &lt;/template&gt;Fetch in `onMounted` and hold the result in a `ref`. As with any front-end, the base URL is a public env var and no secret ever ships to the browser. Point at sandbox in dev"},{"t":"Quickstart: use the Credicorp public API in WordPress","u":"/recipes/quickstart-integrate-public-api-with-wordpress/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from WordPress. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend later for pricing and quotes, always keeping partner secrets server-side.","b":"Call the API from WordPress function credicorp_products() { $cached = get_transient(&#x27;credicorp_products&#x27;); if ($cached !== false) { return $cached; } $res = wp_remote_get(&#x27;https://api.credicorp.co.uk/public/v1/products&#x27;, [ &#x27;timeout&#x27; =&gt; 10, &#x27;headers&#x27; =&gt; [&#x27;Accept&#x27; =&gt; &#x27;application/json&#x27;], ]); if (is_wp_error($res)) { return []; } $data = json_decode(wp_remote_retrieve_body($res), true)[&#x27;data&#x27;]; set_transient(&#x27;credicorp_products&#x27;, $data, HOUR_IN_SECONDS); return $data; }Use `wp_remote_get`, never raw cURL, so "},{"t":"Quickstart: use the Credicorp public API in htmx","u":"/recipes/quickstart-integrate-public-api-with-htmx/","c":"Integration recipes","e":"Quickstart","s":"Here is the idiomatic way to call the Credicorp public API from htmx. Fetch the product catalogue, cache it, and read the base host from configuration — the same shape you extend for pricing, quotes and enquiries, always keeping partner secrets server-side.","b":"Call the API from htmx &lt;div hx-get=&quot;/api/products&quot; hx-trigger=&quot;load&quot; hx-swap=&quot;innerHTML&quot;&gt; Loading finance options\\u2026 &lt;/div&gt;With htmx, point hx-get at your own backend route (which proxies and caches the Credicorp catalogue), and let htmx swap the HTML fragment in on load — no client-side JavaScript to write. Sandbox first Set the base URL to https://sandbox.credicorp.co.uk/public/v1 in development and CI. See choosing a base URL. Next steps Add a quote form or an enquiry submission, and send applicants to apply. Handle failures with the shared error"},{"t":"Quickstart: verify connectivity with the healthz endpoint","u":"/recipes/quickstart-verify-connectivity-with-healthz/","c":"Integration recipes","e":"Quickstart","s":"Before you debug an integration, prove you can reach the API at all with GET /public/v1/healthz. It is a tiny, dependency-free liveness probe that returns quickly and needs no auth — perfect as the first assertion in a smoke test or a deploy gate.","b":"Call it curl -s -o /dev/null -w &#x27;%%{http_code}\\n&#x27; \\ https://api.credicorp.co.uk/public/v1/healthzA 200 confirms DNS, TLS, routing and the API process are all healthy from where you are calling. Any other code points at a network, proxy or firewall issue on the path — not your application code. Use it as a deploy gate Wire it into your release pipeline so a deploy fails fast if the API is unreachable from the new environment:code=$(curl -s -o /dev/null -w &#x27;%{http_code}&#x27; \\ &quot;$CREDICORP_BASE/healthz&quot;) if [ &quot;$code&quot; != &quot;200&quot; ]; then echo &quot;Credic"},{"t":"Quickstart: your first Credicorp public API call with curl","u":"/recipes/quickstart-curl-first-public-api-call/","c":"Integration recipes","e":"Quickstart","s":"The fastest way to see the Credicorp public API working is a single curl call to GET /public/v1/products. The public ring is unauthenticated and rate-limited, so you can list the live business-finance products, read their headline figures and confirm your network path to the API before you write a line of application code.","b":"Make the call The public ring lives under /public/v1 on the API host. It is read-only for product and reference data and accepts unauthenticated requests, so a plain curl is enough to get a real response:curl -s https://api.credicorp.co.uk/public/v1/products \\ -H &#x27;Accept: application/json&#x27;You will get back a JSON object with a data array of products. Each product carries an id, a display name, the type (for example flex or onetime) and headline pricing fields you can render straight into a comparison table. Read the response A trimmed response looks like this:{ &quot;data&quot;: [ { "},{"t":"Quote a Business Loan with the Node SDK","u":"/recipes/quote-with-the-node-sdk/","c":"Integration recipes","e":"Recipe","s":"The Node SDK exposes the public quote endpoint as a typed async call. Await a quote for an amount and term; a range error throws a typed exception you can catch and clamp.","b":"Create and await import { PublicClient } from '@credicorp/sdk'; const credicorp = new PublicClient(); const quote = await credicorp.quote.oneTime({ amount: 300, term: 30, frequency: 'weekly' }); console.log(quote.totalRepayable);The public client needs no token — quotes are on the public ring. Catch the range error try { const quote = await credicorp.quote.oneTime({ amount, term }); } catch (e) { if (e instanceof RangeError) { /* clamp to 50-500 / 14-84 */ } } Front-end or server You can run the public client in a Node server or, for a browser widget, call the endpoint directly with fetch — se"},{"t":"Quote a Business Loan with the PHP SDK","u":"/recipes/quote-with-the-php-sdk/","c":"Integration recipes","e":"Recipe","s":"The PHP SDK wraps the public quote endpoint so you fetch a live figure in three lines. It handles the HTTP, the JSON and the range error — you pass an amount and a term.","b":"Construct and call Point the public client at the hub and call the quote method with an amount and term within the engine range:$credicorp = new Credicorp\\\\PublicClient(); $quote = $credicorp-&gt;quote()-&gt;oneTime(amount: 300, term: 30, frequency: 'weekly'); echo $quote-&gt;totalRepayable; Handle the range error An out-of-range amount or term surfaces as a typed range exception rather than a raw 422 — catch it and clamp:try { $quote = $credicorp-&gt;quote()-&gt;oneTime(amount: $amount, term: $term); } catch (Credicorp\\\\RangeException $e) { // amount 50-500, term 14-84 — clamp and retry } Whe"},{"t":"Quote a Business Loan with the Python SDK","u":"/recipes/quote-with-the-python-sdk/","c":"Integration recipes","e":"Recipe","s":"The Python SDK wraps the public quote endpoint. Construct the client, call quote.one_time(...) with an amount and term, and catch a RangeError for out-of-range inputs.","b":"Construct and call from credicorp import PublicClient credicorp = PublicClient() quote = credicorp.quote.one_time(amount=300, term=30, frequency='weekly') print(quote.total_repayable) Handle out-of-range from credicorp import RangeError try: quote = credicorp.quote.one_time(amount=amount, term=term) except RangeError: ... # clamp to 50-500 / 14-84 and retryThe valid ranges are £50–£500 and 14–84 days. Into an agent Python is a common host for AI agents — pair this with the MCP assistant recipe so your agent quotes real figures and hands off to apply. See choosing an SDK. Does the Python SDK co"},{"t":"RangeGuard","u":"/glossary/range-guard/","c":"Developer glossary","e":"Glossary","s":"RangeGuard is the quote engine's input-bounding step. It enforces the Business Loan range — £50–£500 and 14–84 days — and returns a 422 (or JSON-RPC -32602) when you exceed it.","b":"Definition RangeGuard is the validation that clamps quote inputs to the product's real range before computing. For the Business Loan that is £50–£500 principal and 14–84 days. An out-of-range value is rejected with the endpoint's 422 detail, surfaced as -32602 through the MCP tool. In plain terms The check that stops you asking for a quote outside the allowed amounts and terms. Why it matters here Clamp your inputs to match it and you never see the error. See handling the range error."},{"t":"Rate limit","u":"/glossary/rate-limit/","c":"Developer glossary","e":"Glossary","s":"A rate limit caps how many requests a client may make in a time window. Credicorp’s public ring allows 60 requests per 60 seconds per IP; exceeding it returns a 429.","b":"Definition A rate limit protects a platform from overload and abuse by capping request volume. Credicorp’s public ring uses a fixed window of 60 requests per 60 seconds per IP. Cross it and every further request returns 429 Too Many Requests with a Retry-After header. It is defence-in-depth, not a reliability gate — treat a 429 as a signal to slow down. Is the limit per key or per IP? Per IP on the public ring, which has no credential. See [Rate limits and 429](/reference/rate-limits-and-429/)."},{"t":"Rate limiting","u":"/glossary/rate-limiting/","c":"Developer glossary","e":"Glossary","s":"Rate limiting — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is Rate limiting caps how many requests a caller may make in a time window, protecting the service from overload and abuse. It is defence-in-depth, not a reliability gate — legitimate low-volume callers never notice it. On the Credicorp public ring The public ring uses a fixed-window counter of 60 requests per 60 seconds per IP, per route. Exceed it and further requests return 429 Too Many Requests with a Retry-After header giving the seconds until the window resets. The internal ring is far higher (around 600/60 s per actor). See the rate-limit reference. What happens when I exceed th"},{"t":"Rate limiting across both rings","u":"/public-api/rate-limiting-explained/","c":"Public API guides","e":"Reliability","s":"The public ring is metered at 60 requests per 60 seconds per IP. The partner ring uses a token bucket scoped to your project with sustained and burst capacity that scales by tier, plus tighter per-endpoint sub-limits.","b":"Public ring: a fixed window per IP The public ring caps each caller at 60 requests per 60-second window, keyed to the source IP. It is evaluated at the edge, so a throttled request never touches application code. When you exceed the window you get a 429 — wait for the window to roll and continue. For higher sustained throughput you need a partner project. Partner ring: a token bucket per project The partner ring meters with a token bucket scoped to your project, not to an individual key or token. Each bucket has a sustained refill rate and a burst capacity; a request costs one token and reads "},{"t":"Rate limits and 429 responses","u":"/reference/rate-limits-and-429/","c":"API reference","e":"API reference","s":"The public ring allows 60 requests per 60 seconds per IP. Cross that fixed window and every further request returns 429 Too Many Requests with a Retry-After header. Rate limiting is defence-in-depth, not a reliability gate — it protects the platform without ever silently dropping a legitimate low-volume caller.","b":"The window The public ring uses a fixed-window counter keyed on your IP and the route. Up to 60 requests inside any 60-second slot succeed; the 61st returns 429. When the window rolls over, the counter resets and you can call again. Different route keys count separately, so heavy use of one endpoint does not exhaust another.The internal ring (authenticated /internal/v1/*) is far more generous at 600 requests per 60 seconds per actor, but that ring is out of scope for public integrations. Reading Retry-After Every 429 carries a Retry-After header with the number of seconds until the window rese"},{"t":"RateLimit-* headers","u":"/glossary/rate-limit-headers/","c":"Developer glossary","e":"Glossary","s":"The RateLimit- headers — RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset — appear on every response so you can slow down before hitting the wall.","b":"Definition These IETF-draft headers report the current state of your rate limit on every response, success or failure: the bucket capacity, how many tokens remain, and when it resets. Reading them lets a client pace itself proactively. In plain terms The server telling you, on each reply, how much request budget you have left. Why it matters here Read them and back off before a 429 rather than after. See handling rate limits."},{"t":"Read and render the loyalty ladder","u":"/recipes/read-the-loyalty-ladder/","c":"Integration recipes","e":"Recipe","s":"Render the loyalty ladder from the public loyalty/tiers endpoint (or the MCP loyalty_tiers tool). It carries tier names, thresholds and benefits only — no customer data — and is cacheable.","b":"Read the ladder curl -s https://hub.credicorp.co.uk/public/v1/loyalty/tiersYou get each tier's name, threshold, headline benefit and cooldown — the loyalty endpoint. Agents can read the identical data via loyalty_tiers. Cache it The ladder is config vocabulary with no customer data, so it is explicitly safe to cache. Read it periodically rather than hard-coding the tiers, so a config change flows to your UI on the next fetch. Do not attempt to show a specific customer's tier from this endpoint — that is behind authentication in the portal. Render honestly Present the thresholds and benefits ex"},{"t":"Read the error envelope","u":"/recipes/read-the-error-envelope/","c":"Integration recipes","e":"Recipe","s":"Every failure tells you exactly what went wrong — if you read the envelope. type gives the class, code the specific reason, message a human explanation, and param the offending field. This recipe turns the envelope into a decision.","b":"Parse all four fields Never act on the HTTP status alone.const { type, code, message, param } = (await res.json()).error; log.warn({ status: res.status, code, param, message }); Decide from type + code Use type for the retry/no-retry split and code for the specific behaviour, per the envelope reference. Look up an unfamiliar code in the catalogue. Surface the right thing Show users a message derived from code (never the raw message, which may change), and point form errors at the field named in param. Why not just show error.message to users? The `message` is for developers and may be reworded"},{"t":"Read-only tool","u":"/glossary/read-only-tool/","c":"Developer glossary","e":"Glossary","s":"Read-only tool — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is A read-only tool can fetch and report information but cannot change state. It is safe to expose to an AI agent because, however it is called, it cannot cause a side effect. In the Credicorp API All six tools on the Credicorp MCP server — ListProducts, ProductDetails, GetQuote, EligibilityCriteria, HowToApply, LoyaltyTiers — are read-only. An agent can describe products or quote indicatively, but it can never apply, transact or move money. That boundary is absolute on the public ring. Can any MCP tool change my data? No. Every Credicorp MCP tool is read-only. It informs; it never act"},{"t":"Receive your first webhook","u":"/recipes/receive-your-first-webhook/","c":"Integration recipes","e":"Recipe","s":"Go from nothing to a verified webhook in four steps: expose an HTTPS endpoint, register it, verify the Credicorp-Signature, and return 200. This recipe wires the minimum handler you can build on.","b":"1. Expose an HTTPS endpoint Any framework works. The handler must read the raw body before parsing — you need the exact bytes to verify the signature (see signature verification).app.post('/credicorp/webhooks', express.raw({type:'*/*'}), (req, res) => { const sig = req.header('Credicorp-Signature'); if (!verify(req.body, sig, process.env.WHSEC)) return res.status(400).end(); const evt = JSON.parse(req.body); enqueue(evt); // do real work async res.status(200).end(); // acknowledge within 10s }); 2. Register it Register the URL and subscribe to the events you care about. See Register a webhook "},{"t":"Record cookie consent from your edge","u":"/recipes/record-cookie-consent/","c":"Integration recipes","e":"Recipe","s":"Record a visitor’s cookie choices to the consent endpoint from your server, not the browser. When someone interacts with your cookie banner, have your edge validate the choice and forward it to POST /public/v1/consent. The record is appended to a PECR audit trail — never overwritten — so you always have evidence of what was agreed and when.","b":"Step 1 — validate at your edge Your server is the trust boundary. Validate the visitor’s banner interaction first — the endpoint accepts the forwarded snapshot without a CSRF check precisely because your edge already did that check. Step 2 — forward the snapshot curl -sS -X POST https://hub.credicorp.co.uk/public/v1/consent \\ -H 'Content-Type: application/json' \\ -d '{\"analytics\":true,\"marketing\":false}' Step 3 — it is appended, not replaced Every snapshot is appended to the PECR audit trail. A change of mind is a new record on top, not an edit — so the history is complete and tamper-evident. "},{"t":"Register a webhook endpoint","u":"/reference/register-a-webhook-endpoint/","c":"API reference","e":"API reference","s":"A webhook endpoint is the HTTPS URL Credicorp POSTs events to. You register one per integration, choose which event types it subscribes to, and receive a signing secret used to verify every delivery. An endpoint must be HTTPS, must answer on the public internet, and must return a <code>2xx</code> to acknowledge receipt.","b":"The endpoint object An endpoint is described by this object. The secret is returned once at creation and never again — store it immediately.{ \"id\": \"whend_3T8K1\", \"url\": \"https://api.your-app.example/credicorp/webhooks\", \"enabled_events\": [ \"enquiry.created\", \"decision.completed\", \"payment.succeeded\" ], \"status\": \"enabled\", \"secret\": \"whsec_a1b2c3\\u2026\", \"created\": \"2026-07-04T09:00:00Z\" } Choosing events Set enabled_events to the specific types you handle, or [\"*\"] to receive everything. Subscribing narrowly reduces traffic and keeps your handler simple. The full list is in the event catalog"},{"t":"Render a live Business Loan quote widget","u":"/recipes/render-a-live-quote-widget/","c":"Integration recipes","e":"Recipe","s":"Wire a small front-end control to the public quote endpoint so visitors get a live, accurate Business Loan figure. Clamp inputs to £50–£500 / 14–84 days and handle the 422 gracefully.","b":"Call the endpoint The quote lives at GET /public/v1/quote/onetime. It takes amount, term and frequency:const q = await fetch( `https://hub.credicorp.co.uk/public/v1/quote/onetime?amount=${amount}&amp;term=${term}&amp;frequency=${freq}` ).then(r =&gt; r.json());Because the ring is unauthenticated and CORS-friendly for reads, you can call it straight from the browser. Clamp before you call Constrain your slider or inputs to the engine's range up front — £50–£500 and 14–84 days — so a user cannot request a figure the engine will reject. If a value still slips through, the endpoint returns a 422 w"},{"t":"Render the loyalty ladder on your site","u":"/recipes/render-the-loyalty-ladder/","c":"Integration recipes","e":"Recipe","s":"Show the Credicorp loyalty ladder on any page from one cached call. Fetch the four tiers, render them Bronze→Platinum, and display each tier’s arrangement-fee discount as a percent (remember: 0.5 means 0.5%). Cache the response for a few minutes and you add almost no load.","b":"Step 1 — fetch the tiers curl -sS https://hub.credicorp.co.uk/public/v1/loyalty/tiers Step 2 — render in order The tiers array is already in Bronze→Platinum order — render it as-is. For each tier show the name, the qualification thresholds, and the arrangement_fee_discount_pct. That figure is in percent units, so print it with a % sign directly; do not multiply by 100. Step 3 — cache it The ladder rarely changes. Cache the response for around five minutes keyed on the generated timestamp; see Cache the loyalty ladder. Doing it via MCP instead If an AI agent is your consumer, the same data is a"},{"t":"Replay and recover missed webhooks","u":"/recipes/replay-and-recover-missed-webhooks/","c":"Integration recipes","e":"Recipe","s":"If your endpoint was down, you have options. Automatic retries cover a 72-hour window, you can manually redeliver specific events, and for anything past the window you reconcile by re-reading current state from the API.","b":"Lean on automatic retries Brief outages need no action — deliveries retry on exponential backoff for 72 hours. Once your endpoint returns 2xx, queued events flow through. Redeliver specific events For a known-missed event, trigger a manual redelivery from the partner dashboard. It is re-signed with a fresh timestamp, so verification passes normally. Reconcile past the window Events older than 72 hours are gone from the queue. Recover by re-reading the affected resources from the API and computing the delta against your own records. Your idempotent handler makes re-applying safe. How long do I "},{"t":"Replay attack","u":"/glossary/replay-attack/","c":"Developer glossary","e":"Glossary","s":"A replay attack re-sends a captured, validly-signed request to trigger its effect again. Credicorp defends against it with a signed timestamp and a 5-minute tolerance on webhooks.","b":"Definition In a replay attack, an attacker captures a legitimate signed request and re-transmits it later. Because it was genuinely signed, a naive verifier accepts it. Credicorp includes the timestamp inside the HMAC and rejects anything outside a 300-second window, so a stale replay fails verification. Pair that with idempotency on the event id and a replay is harmless even within the window. How does the timestamp stop replays? It is signed, so it cannot be altered, and old timestamps are rejected. A captured request goes stale within five minutes. See [signature verification](/reference/we"},{"t":"Representative quote","u":"/glossary/representative-quote/","c":"Developer glossary","e":"Glossary","s":"A representative quote is an illustrative, non-binding figure for a Business Loan at a given amount and term. It shows the shape of the cost, not a committed offer.","b":"Definition The quote endpoint and the get_quote tool return a representative figure computed by the same engine. It illustrates cost within the public range (£50–£500, 14–84 days) but is not a binding offer — a real offer follows an application and decision. In plain terms An accurate example figure, not a promise of those exact terms. Why it matters here Use it to inform, then hand the user to apply for a real offer. See the quote widget."},{"t":"Request body","u":"/glossary/request-body/","c":"Developer glossary","e":"Glossary","s":"Request body — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is A request body is the data a client sends with a request, typically JSON for a POST. The server parses and validates it before acting. In the Credicorp API On the public ring, enquiries and consent take a JSON body with a Content-Type: application/json header. GET endpoints take no body. Keep bodies within the documented size limits (an enquiry’s fields must be ≤16 KiB). Do GET endpoints take a request body? No. The public GET endpoints — loyalty tiers, CMS pages, billers — take no body. Only the POST endpoints (enquiries, consent, support chat, MCP) carry one."},{"t":"Request signing (CC1-HMAC)","u":"/glossary/request-signing/","c":"Developer glossary","e":"Glossary","s":"Request signing is the HMAC (CC1) signature Credicorp uses to authenticate inter-site control hops — for example a marketing site submitting a consent or enquiry over a signed request.","b":"Definition Some public mutations are reached over a CC1-HMAC-signed hop: the calling site signs the request with a per-host shared secret so the hub can attribute it, even though the public endpoint itself needs no user auth. It is the same HMAC family used to verify webhooks, applied in the outbound direction. In plain terms A signature on inter-site requests that proves which trusted site sent them. Why it matters here You mostly encounter it if you host a Credicorp-integrated marketing site; the enquiries and consent hops use it."},{"t":"Respond to a 401 cleanly","u":"/recipes/respond-to-a-401-cleanly/","c":"Integration recipes","e":"Recipe","s":"A partner 401 has two causes: an expired token (re-mint and retry once) or an invalid credential (fix it — do not loop). Telling them apart is the difference between resilience and a retry storm.","b":"Two very different 401s A 401 on the partner ring means your token was not accepted. If it merely expired between your check and the call, the fix is to mint a fresh token and retry the request exactly once. If the credential is wrong or revoked, re-minting fails too — retrying just burns your token-endpoint quota. Handle it in code let res = await call(token); if (res.status === 401 &amp;&amp; !alreadyRetried) { token = await mintToken(); // re-mint once res = await call(token); // retry exactly once } if (res.status === 401) throw new AuthError('credential invalid');The one-shot retry covers"},{"t":"Response envelopes and shapes","u":"/public-api/response-envelopes-and-shapes/","c":"Public API guides","e":"Public API","s":"Every public-API response follows a predictable shape, so parsing is simple. Reads return a resource object (often with a generated timestamp); a successful enquiry returns {id, status}; and every error returns an {error:{code, message}} envelope. Parse defensively — ignore unknown fields — and your integration survives additive change.","b":"Success shapes Reads return the resource directly — a tiers array, a billers list, a CMS {key,title,html,updated}. Cacheable reads add a generated UTC timestamp. An enquiry returns {id, status:\"new\"}. The error envelope Every non-2xx response is {\"error\":{\"code\":\"...\",\"message\":\"...\"}}. Branch on the stable code; treat message as human-facing. See Errors and status codes. Parse defensively Ignore fields you do not recognise, do not assume array order beyond what is documented, and never fail on an added field. That is how you stay compatible with additive change. See Versioning. Is the error s"},{"t":"Retry a write safely with an idempotency key","u":"/recipes/retry-a-write-safely-with-an-idempotency-key/","c":"Integration recipes","e":"Recipe","s":"A retried write is dangerous without an idempotency key. If the first request succeeded before the connection dropped, a naive retry creates a duplicate. Send an Idempotency-Key: the server recognises the replay and returns the original result instead of acting twice.","b":"Send a key per operation Generate a unique key (a UUID) for each distinct write, and reuse that same key only when retrying that same request.curl -sS -X POST https://hub.credicorp.co.uk/public/v1/enquiries \\ -H 'Content-Type: application/json' \\ -H 'Idempotency-Key: 5f2c9a1e-8b3d-4a7c-9e21-0d6f1b2c3a4e' \\ -d '{\"form\":\"contact-us\",\"fields\":{\"consent\":\"yes\"}}' What the server does On first receipt it processes and records the result under the key. On a replay with the same key and same body, it returns the stored result — no duplicate. A same key with a different body returns 409 idempotency_ke"},{"t":"Retry-After header","u":"/glossary/retry-after-header/","c":"Developer glossary","e":"Glossary","s":"Retry-After header — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is Retry-After is a standard HTTP response header that tells a client how long to wait before making another request. The Credicorp public API sends it with every 429 Too Many Requests, expressed as a number of seconds until the rate-limit window resets. How to use it On a 429, read Retry-After, sleep for that many seconds (add a little jitter if you run many clients), then retry once. Ignoring it and retrying immediately only keeps the window saturated. See handling rate limits gracefully. Do I have to honour Retry-After? You should. Waiting the stated time is the fastest path back to"},{"t":"Retrying failed requests safely","u":"/reference/retrying-failed-requests/","c":"API reference","e":"API reference","s":"Retry only the retry-able. 429 and 5xx are transient — back off and try again. 4xx other than 429 will fail identically on retry, so fix the request instead. Use exponential backoff with jitter, cap your attempts, honour Retry-After on a 429, and pair retries with an <a href=\"/glossary/idempotency-explained/\">idempotency key</a> so a retried write never double-applies.","b":"Decide first StatusRetry?Why429Yes, after Retry-AfterTransient — the window resets500/502/503/504Yes, with backoffTransient server-side400/404/409/422NoDeterministic — will fail again401/403NoFix the credential first Backoff with jitter async function withRetry(fn, max = 5) { for (let attempt = 0; ; attempt++) { const res = await fn(); if (res.status < 400) return res; const retryable = res.status === 429 || res.status >= 500; if (!retryable || attempt >= max) return res; const ra = Number(res.headers.get('Retry-After')) || 0; const backoff = Math.min(2 ** attempt, 32); const jitter = Math.ran"},{"t":"Rotate a webhook signing secret","u":"/recipes/rotate-a-webhook-signing-secret/","c":"Integration recipes","e":"Recipe","s":"Rotate the signing secret if it may have leaked — with no downtime. Rotation issues a new whsec_ secret while the old one stays valid for a 24-hour overlap. Verify against both during the window, then drop the old one.","b":"Trigger rotation Rotate from the partner console or API. You receive the new whsec_ secret; the old one keeps working for 24 hours so in-flight deliveries never fail. Verify against both During the overlap, accept a delivery if it verifies under either secret. Requests may also carry multiple v1= values — a match on any is valid (see signature verification).function verifyAny(raw, header, secrets) { return secrets.some(s => verify(raw, header, s)); } Retire the old secret Once the 24 hours pass, remove the old secret from your config. Verification continues seamlessly under the new one. Will I"},{"t":"Rotate partner API credentials with zero downtime","u":"/recipes/rotate-partner-api-credentials/","c":"Integration recipes","e":"Recipe","s":"Rotate a partner secret with no outage: create a second credential with the same scopes, cut traffic to it, verify, then revoke the old one. Never edit a live credential in place.","b":"Overlap, then cut over The safe pattern is overlap. Mint a second OAuth client with the same scopes as the current one, deploy it to your token cache, and let both work for a short window. Because tokens are minted from the credential, your running tokens keep working during the swap. Verify on the new credential Confirm a real call succeeds on a token minted from the new client before you touch the old one — mint, call a read endpoint, check for a 200. Only then move all traffic across. Keep the token cache keyed by client so the two never collide (see caching tokens). Revoke the old credenti"},{"t":"Route enquiries to the right department","u":"/recipes/route-enquiries-to-the-right-department/","c":"Integration recipes","e":"Recipe","s":"Send each enquiry to the right team with the dept field. Set dept to support, payments, hardship or complaints and the enquiry is notified to that department’s inbox. Omit it and the enquiry goes to the general inbox. The record is saved either way — routing is a notification, not a gate.","b":"Pick the department Map your form to a dept: general contact → support, a billing question → payments, a customer in difficulty → hardship, a formal complaint → complaints. Each has its own department inbox. The record is always saved The enquiry is persisted first and notified second. A mail hiccup never affects the saved record or your 201. See the enquiries reference. Handle hardship with care If your form serves customers in financial difficulty, route to hardship and keep the flow low-friction — this is a moment to help, not to upsell. What are the department values? `support`, `payments`"},{"t":"Route webhook events by type","u":"/recipes/route-webhook-events-by-type/","c":"Integration recipes","e":"Recipe","s":"One endpoint, many event types. Dispatch on the envelope type to a small handler per event, and no-op anything you do not recognise so new event types never break you.","b":"Dispatch table Map each type to a handler; fall through to a no-op.const handlers = { 'enquiry.created': onEnquiryCreated, 'decision.completed': onDecisionCompleted, 'payment.failed': onPaymentFailed, }; function dispatch(evt) { (handlers[evt.type] || (() => {}))(evt.data.object); } Ignore unknown types New types are added additively (see the catalogue). Never throw on an unrecognised type — that turns a harmless new event into a delivery failure and a retry storm. Keep handlers idempotent Each handler still runs behind the idempotency guard keyed on the event id. Should I 500 on an unknown ty"},{"t":"SDK","u":"/glossary/sdk/","c":"Developer glossary","e":"Glossary","s":"An SDK is a language library that wraps the Credicorp REST API — handling auth, retries, pagination and typing — so you write business logic, not plumbing. Available for PHP, Node and Python.","b":"Definition A Credicorp SDK exposes the same endpoints as typed methods and manages the tedious mechanics: minting and caching tokens, back-off on 429/5xx, and cursor iteration. In plain terms A ready-made library so you do not hand-roll the HTTP and auth. Why it matters here Use one for partner flows; raw HTTP is fine for a single public read. See choosing an SDK or raw HTTP."},{"t":"Sandbox","u":"/glossary/sandbox/","c":"Developer glossary","e":"Glossary","s":"The sandbox is an isolated partner project for building and testing — its own credentials and rate-limit bucket, fixed at the build tier, never touching live data or quotas.","b":"Definition The sandbox is a separate project with its own OAuth client and bucket. It is fixed at the build tier (10 req/s, burst 50) so it is for correctness, not load. Sandbox and live never share credentials, data or quotas. In plain terms A safe copy of the API to build against before you go live. Why it matters here The public ring has no sandbox — it returns only published data. See environments and the sandbox."},{"t":"Sandbox","u":"/glossary/sandbox-testing/","c":"Developer glossary","e":"Glossary","s":"Sandbox — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is A sandbox is a non-production environment where you can test an integration without affecting real data. In the Credicorp API Because the public reads carry no PII and change nothing, you can safely explore them against the live endpoint. The write endpoints (enquiries, consent) record real data, so use clearly-marked test values and avoid flooding them. Partner integrations that need a full sandbox are covered in the partner sandbox docs. Can I test the public API against live? The reads yes — they change nothing and carry no PII. For writes, use obvious test values and do not floo"},{"t":"Scope","u":"/glossary/scope/","c":"Developer glossary","e":"Glossary","s":"A scope is a named capability on an OAuth token — like applications:write — that bounds what the token can do. Request only the scopes you use.","b":"Definition A scope names a capability a token is allowed to exercise. You request scopes when you mint an access token, and the token can do nothing outside them — a call to an out-of-scope capability returns a 403. In plain terms The list of things a particular token is permitted to do. Why it matters here Scoping narrowly bounds the blast radius of a leaked credential — the heart of least privilege."},{"t":"Scopes and least privilege on partner/v1","u":"/public-api/scopes-and-least-privilege/","c":"Public API guides","e":"OAuth 2.0","s":"Request only the scopes your integration actually uses. A credential scoped to applications:write cannot read decisions or move money — that blast-radius reduction is the whole point of least privilege.","b":"Scopes name capabilities Each partner capability maps to a scope — reading applications, writing applications, reading decisions, provisioning payments, running identity checks. You name the scopes you want on the token request, and the minted token can only exercise those. A token that never asked for a payments scope simply cannot provision a payment link, so a leaked token is bounded by what it was allowed to do. Why least privilege The cost of over-scoping is entirely downside: a credential that can do everything is a credential that, if compromised, can do everything. Scope narrowly and a"},{"t":"Secure a webhook endpoint","u":"/recipes/secure-a-webhook-endpoint/","c":"Integration recipes","e":"Recipe","s":"A webhook endpoint is an inbound door — lock it. Enforce HTTPS, verify every signature over the raw body, reject stale timestamps to blunt replay, keep the signing secret out of logs, and subscribe to only the events you handle.","b":"Enforce transport and auth Only accept HTTPS. Verify the HMAC signature before parsing, and reject timestamps outside the 300-second window to stop replays. Protect the secret Store the whsec_ secret in a secrets manager, never in source or logs. Rotate it immediately if it may have leaked. Minimise exposure Subscribe to only the event types you handle, run the endpoint on a dedicated path, and rate-limit it yourself as defence-in-depth. Return generic errors — do not leak internal detail in the response body. Is the webhook body encrypted? It is protected in transit by HTTPS. The signature pr"},{"t":"Security best practices for integrators","u":"/public-api/security-best-practices/","c":"Public API guides","e":"Security","s":"Five habits keep an integration safe: guard the secret, scope narrowly, verify every webhook, TLS everywhere, and rotate on a schedule. None is optional in production.","b":"Protect the credential The client_secret is the keys to the partner ring. Keep it server-side only, in a secrets manager or environment variable — never in a repo, a browser bundle or a mobile app. Scope every token to only what it uses (least privilege), so a leak is bounded, and rotate on a schedule. Verify what comes in Never trust an unverified webhook — recompute its HMAC signature over the raw body in constant time before acting. Reject on mismatch. This is the single most important control on the receiving side, because a webhook drives real state in your system. Transport and hygiene U"},{"t":"Security model of the public ring","u":"/public-api/security-model-of-the-public-ring/","c":"Public API guides","e":"Public API","s":"An unauthenticated ring can be secure — here is exactly how this one is. With no credential to lean on, the public ring stacks five defences: per-IP rate limiting, strict input validation, server-fixed response shapes, a hard no-PII boundary, and server-side HTML sanitisation. Together they let the platform accept traffic from the open internet safely.","b":"Rate limiting 60 requests per 60 seconds per IP caps abuse and keeps one caller from monopolising the ring. See rate limits. Validation Every input is bounded in type, length, key count and byte size, and mandatory gates like consent are enforced server-side. A bad request is a 422, not an action. Fixed shapes The server owns privileged fields. A caller cannot set an enquiry’s status or coax bank fields into a biller response — they are simply not settable. PII boundary No per-customer data crosses the ring, so there is nothing sensitive to leak even under a determined probe. See Privacy and P"},{"t":"Semantic versioning","u":"/glossary/semantic-versioning/","c":"Developer glossary","e":"Glossary","s":"Semantic versioning — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is Semantic versioning encodes compatibility in a version number: MAJOR for breaking changes, MINOR for backwards-compatible additions, PATCH for fixes. It tells a consumer at a glance whether an upgrade is safe. At Credicorp The API’s /v1 path is the MAJOR version — a stability contract that additive change stays within and breaking change would leave. The MCP server reports its own semantic version (1.0.0). See Versioning and stability. What does the /v1 guarantee? That existing fields keep their name, type and meaning. New fields and endpoints may be added, but nothing you depend on"},{"t":"Set up a webhook endpoint","u":"/recipes/set-up-a-webhook-endpoint/","c":"Integration recipes","e":"Recipe","s":"A production webhook endpoint needs five things: a public HTTPS URL, signature verification, a fast 2xx, async processing, and replay safety. Miss any one and delivery gets flaky.","b":"Expose and register Stand up a public HTTPS endpoint (Credicorp will not deliver to plain HTTP) and register it. In development, tunnel a local server to a public URL so you can receive real deliveries. The endpoint receives the signed events you subscribed to. Verify, ack fast, process async On each delivery: verify the signature over the raw body, enqueue the event, and return a 2xx immediately. Do the heavy work in a background job — a slow synchronous handler risks a timeout, which Credicorp reads as failure and retries, multiplying duplicates. Make it replay-safe Because delivery is at-le"},{"t":"Signature header (Credicorp-Signature)","u":"/glossary/signature-header/","c":"Developer glossary","e":"Glossary","s":"Credicorp-Signature is the header on every webhook carrying a signed timestamp (t=) and one or more HMAC-SHA256 values (v1=) you verify against your signing secret.","b":"Definition The Credicorp-Signature header is how you prove a webhook is genuine. It carries the timestamp the signature was made and the HMAC of {t}.{rawbody}. You recompute the HMAC with your signing secret and compare in constant time, rejecting anything outside the 300-second tolerance. Multiple v1= values can appear during a secret rotation. See signature verification. Why multiple v1 values? During a secret rotation, deliveries are signed with both the old and new secret so you never miss one. A match on any `v1=` is valid."},{"t":"Smoke-test the public API in 60 seconds","u":"/recipes/smoke-test-the-public-api/","c":"Integration recipes","e":"Recipe","s":"Confirm the public ring works before you build: three unauthenticated curls — healthz, products, quote — prove reachability, the catalogue and the quote engine in under a minute.","b":"1. Is the ring up? curl -s https://hub.credicorp.co.uk/public/v1/healthzA 200 confirms the hub is serving. A 503 means it is not ready (it fails closed on migration drift) — see healthz. 2. What products exist? curl -s https://hub.credicorp.co.uk/public/v1/productsYou should see Business Loan, Credicorp Flex and Credicorp Slice with their enabled flags — the products endpoint. 3. Does the quote engine answer? curl -s 'https://hub.credicorp.co.uk/public/v1/quote/onetime?amount=300&amp;term=30&amp;frequency=weekly'A £300 / 30-day quote lands within range (£50–£500, 14–84 days). Try amount=1000 t"},{"t":"Stale-while-revalidate","u":"/glossary/stale-while-revalidate/","c":"Developer glossary","e":"Glossary","s":"Stale-while-revalidate — a term used across the Credicorp public-API documentation. The definition below is written for engineers integrating the /public/v1 ring.","b":"What it is Stale-while-revalidate (SWR) is a caching strategy where an expired cache entry is served immediately to the user while a fresh copy is fetched in the background for next time. The user never waits on the origin, and the cache is at most one cycle behind. For the public API SWR pairs well with the cacheable public reads — loyalty tiers, billers, CMS pages. Serve the stale ladder instantly, refresh behind the scenes, and your pages stay fast without ever showing badly outdated data. See caching the loyalty ladder. Is SWR safe for the loyalty endpoint? Yes. The data changes rarely and"},{"t":"Submit a contact form to the enquiries endpoint","u":"/recipes/submit-a-contact-form/","c":"Integration recipes","e":"Recipe","s":"Wire any contact form to POST /public/v1/enquiries in a few lines. Collect the visitor’s answers into a flat fields object, set fields.consent to \"yes\", pick a form key and an optional dept, and post it. On success you get a 201 with the enquiry id; on a validation slip you get a 422 you can surface back to the user.","b":"Step 1 — build the payload Keep fields flat — string, number or boolean values only, up to 60 keys, ≤16 KiB encoded. Include a valid email if you collect one, and always set consent.{ \"form\": \"contact-us\", \"dept\": \"support\", \"fields\": { \"name\": \"Jordan Ellis\", \"email\": \"jordan@example-ltd.co.uk\", \"company\": \"Example Trading Ltd\", \"message\": \"Please call me about working capital.\", \"consent\": \"yes\" } } Step 2 — post it curl -sS -X POST https://hub.credicorp.co.uk/public/v1/enquiries \\ -H 'Content-Type: application/json' \\ -d @enquiry.json Step 3 — handle the response A 201 returns {id, status:\""},{"t":"Submit an application on partner/v1","u":"/recipes/submit-an-application-partner-v1/","c":"Integration recipes","e":"Recipe","s":"Take an application end to end on the partner ring: mint a token, POST /applications with an idempotency key, then read the decision. Retry safely and never double-submit.","b":"1. Mint a scoped token Get a token scoped to what you need (see client credentials):TOKEN=$(curl -s -X POST https://hub.credicorp.co.uk/partner/v1/oauth/token \\ -d grant_type=client_credentials -d client_id=$ID -d client_secret=$SECRET \\ -d scope='applications:write decisions:read' | jq -r .access_token) 2. Submit with an idempotency key curl -s -X POST https://hub.credicorp.co.uk/partner/v1/applications \\ -H 'Authorization: Bearer '$TOKEN \\ -H 'Idempotency-Key: '$(uuidgen) \\ -H 'Content-Type: application/json' -d @application.jsonThe idempotency key means a timed-out retry returns the origina"},{"t":"Test a webhook locally with a tunnel","u":"/recipes/test-a-webhook-locally-with-a-tunnel/","c":"Integration recipes","e":"Recipe","s":"Test webhooks against local code without deploying. Expose your local server through a tunnel, register the tunnel URL as a sandbox endpoint, then drive test-mode actions and watch the deliveries hit your handler.","b":"1. Tunnel localhost Run any HTTPS tunnel that maps a public URL to your local port. Copy the public HTTPS URL it prints.your-tunnel http 3000 # -> https://random-name.tunnel.example -> localhost:3000 2. Register the tunnel URL Register it as an endpoint in test mode and subscribe to * so you see everything. See Register a webhook endpoint. 3. Drive test events Trigger the corresponding action in the sandbox — submit a test enquiry, run a test decision. Deliveries arrive with livemode:false so you can safely branch test vs live. 4. Inspect and iterate Log the raw body and the verification resul"},{"t":"Testing your integration","u":"/public-api/testing-your-integration/","c":"Public API guides","e":"Testing","s":"Test in layers: smoke-test the public ring, build against the sandbox, simulate webhooks and errors, then gate your release on all three. The rings make each layer cheap.","b":"Layer 1: public smoke test Before anything else, confirm the ring is reachable and your assumptions about shapes hold — three unauthenticated calls (healthz, products, quote). This catches connectivity and CORS problems in seconds, with no credentials. Layer 2: the sandbox project Build partner flows against the sandbox project — its own credentials and its own bucket, fixed at the build tier — so a test run never touches live data or quotas. Verify auth, application submission, decision reads and payment provisioning end to end here. Layer 3: failure paths Test the unhappy paths deliberately:"},{"t":"Testing your public-API integration","u":"/public-api/testing-your-public-api-integration/","c":"Public API guides","e":"Public API","s":"Test your integration confidently without a special sandbox. The public reads change nothing and carry no PII, so you can assert against them live. For writes, use clearly-marked test data and check the status and error codes. Keep test loops under the rate limit, and you have a fast, honest test suite.","b":"Test the reads live Loyalty tiers, CMS pages and billers change nothing and carry no PII — assert on their shapes directly against the live endpoint. Check the status is 200, the expected fields exist, and the generated timestamp is recent. Handle writes carefully Enquiries and consent record real data. Use obvious test values (a clearly fake company name, a role-based test email), assert on the 201, and do not run write tests in a tight loop. See sandbox. Assert on codes, not messages Branch tests on the HTTP status and the error code, which are stable. Do not assert on the human message — it"},{"t":"The Credicorp MCP server","u":"/public-api/mcp-server-overview/","c":"Public API guides","e":"MCP","s":"Credicorp runs an MCP server at /public/v1/mcp — JSON-RPC 2.0 over HTTP exposing the product catalogue, quotes, eligibility, how-to-apply and loyalty tiers as tool calls an AI agent can invoke directly.","b":"What MCP is here for The Model Context Protocol is an open standard for exposing tools to language-model agents. Credicorp's public MCP server lets an assistant fetch accurate, live Credicorp figures — products, a representative quote, eligibility criteria, the application steps and the loyalty ladder — without scraping the marketing site or hallucinating numbers. It sits on the unauthenticated public ring, so an agent can call it with no credential. The tool catalogue ToolReturnslist_productsThe three products with names + one-liners + enabled flagproduct_detailsA product's published descript"},{"t":"The Credicorp MCP server, explained","u":"/public-api/the-mcp-server-explained/","c":"Public API guides","e":"Public API","s":"The Credicorp MCP server lets an AI agent read public business-lending facts as tool calls. It speaks the Model Context Protocol (revision 2025-06-18), identifies as Credicorp v1.0.0, and exposes six strictly read-only tools — list products, product details, indicative quote, eligibility, how to apply, and loyalty tiers. It can inform an agent; it can never act on an account or move money.","b":"Why an MCP server The Model Context Protocol is a standard way for AI agents to discover and call tools. By exposing Credicorp’s public knowledge as an MCP server, any compliant agent — a chatbot, a research assistant, a customer’s own copilot — can answer questions about our products accurately, from the source, instead of guessing. The endpoint lives on the public ring at /public/v1/mcp, so there is no credential to connect. Read-only by design Every tool reads; none writes. The server can tell an agent what products exist, what a quote might look like, and how to apply — but it cannot submi"},{"t":"The Credicorp public API, end to end","u":"/public-api/public-api-overview/","c":"Public API guides","e":"Public API","s":"The /public/v1 ring is Credicorp's unauthenticated, read-mostly surface — product figures, quotes, the loyalty ladder, an MCP server and a lead-intake endpoint. No token, no customer data, safe to cache.","b":"What the public ring is for The public ring exposes the same published figures Credicorp uses on its own marketing estate — the product catalogue, representative quotes, pricing config and the loyalty tier ladder — over plain HTTP. Because everything it returns is already public, there is no authentication on the read endpoints and no customer data anywhere in the surface. It is the right layer for comparison sites, AI agents and anyone who wants to render an accurate Credicorp quote without holding a partner credential.The base URL is https://hub.credicorp.co.uk/public/v1. Responses are appli"},{"t":"The MCP tool catalogue in depth","u":"/public-api/mcp-tools-catalogue/","c":"Public API guides","e":"MCP","s":"The public MCP server exposes six read-only tools. Together they let an agent answer almost any question about Credicorp products, eligibility, cost and rewards from live, authoritative figures.","b":"Catalogue and product tools list_products returns the three products — Business Loan, Credicorp Flex and Credicorp Slice — each with its real name, a one-line description and a flag for whether it is currently enabled. product_details takes a product and returns its published description plus byte-exact representative figures. Use list_products to discover what exists, then product_details to go deep on one. Full reference: list_products and product_details. The quote tool get_quote computes a representative Business Loan quote for an amount and term. Only the Business Loan is quotable via thi"},{"t":"The access-token lifecycle","u":"/public-api/token-lifecycle/","c":"Public API guides","e":"OAuth 2.0","s":"A partner access token is minted, cached, reused until just before expiry, then re-minted. There is no refresh token in the client-credentials grant — you simply request a new one.","b":"The full cycle The lifecycle is a short loop. Your server posts its client credentials to the token endpoint, caches the returned token alongside its expiry, and attaches it to every partner call. Shortly before the token expires it mints a fresh one and swaps it in. Because there is no user session, there is no refresh token — the client-credentials grant re-mints from the same secret each time. Caching correctly Key the cache by client and scope, and store the absolute expiry (now + expires_in, minus a safety margin of a few seconds). Serve the cached token to every worker rather than mintin"},{"t":"The error envelope and status codes","u":"/reference/error-envelope-and-status-codes/","c":"API reference","e":"API reference","s":"Every API error uses one envelope. A non-2xx response returns {\"error\":{...}} with a stable type, a machine-readable code, a human message and an optional param. The HTTP status tells you the broad class; the code tells you exactly what went wrong. Branch on code, log the message, and show the user something sensible.","b":"The envelope { \"error\": { \"type\": \"invalid_request_error\", \"code\": \"consent_required\", \"message\": \"fields.consent must equal \\\"yes\\\".\", \"param\": \"fields.consent\" } }FieldTypeNotestypestringBroad class: invalid_request_error, authentication_error, rate_limit_error, api_error.codestringSpecific, stable machine code. This is what you branch on.messagestringHuman-readable. Safe to log; do not parse.paramstring?The offending field, when the error is about one input. Status → class StatustypeMeaningRetry?400invalid_request_errorMalformed request (bad JSON, wrong shape)No — fix the request401authenti"},{"t":"The partner API, at a glance","u":"/public-api/partner-v1-overview/","c":"Public API guides","e":"Partner API","s":"The /partner/v1 ring is the token-gated integration API — take applications, read decisions, provision payments and run identity checks. Auth is OAuth 2.0 client credentials; quotas scale with your partner tier.","b":"What lives on partner/v1 The partner ring is where the real integration work happens: submitting an application (POST /applications), reading the decision (GET /decisions/{id}), provisioning a payment link over PISP (POST /payments/links) and running a KYC/AML identity check (POST /identity/checks). Each of these touches a customer, a decision or the rails, which is exactly why the ring is authenticated. Money-out remains Credicorp's single manual gate regardless of tier — the API can request a disbursement, but the release stays governed. Authentication Every partner call carries a bearer acc"},{"t":"The partner MCP server","u":"/public-api/partner-mcp-server/","c":"Public API guides","e":"MCP","s":"There are two MCP servers. The public one on /public/v1/mcp needs no token and exposes published figures. The partner one on /partner/v1/mcp sits behind OAuth and exposes authenticated capabilities.","b":"Why two servers The public MCP server is deliberately open — it publishes figures an agent may need with no credential. But an agent acting on behalf of a partner — reading applications, checking a decision — needs authentication and scope, so those capabilities live on a separate token-gated server at /partner/v1/mcp. The protocol is identical; the difference is the Authorization: Bearer header and the OAuth-protected-resource metadata. Discovery and auth The partner MCP server advertises its auth requirements through the standard /.well-known/oauth-protected-resource document, which points M"},{"t":"The public /public/v1 ring, explained","u":"/public-api/the-public-v1-ring/","c":"Public API guides","e":"Public API","s":"The /public/v1 ring is the unauthenticated, anonymous surface of the Credicorp API. It exposes exactly the things a website, widget or AI agent can safely read or submit without a login — product data, the loyalty ladder, published CMS copy, enquiry submission, cookie consent, the support widget and a read-only MCP server. It is protected by rate limiting and strict validation rather than by a credential, and it deliberately never touches money, accounts or credit decisions.","b":"What \"public ring\" means The Credicorp API is organised into rings by trust level. The /public/v1 ring is the outermost: anyone can call it, from any origin, with no API key and no OAuth token. That is by design — everything on this ring is either public information (products, tiers, published pages) or a submission that is safe to accept from the open internet under strict validation (an enquiry, a cookie-consent snapshot).Inside the public ring sits the /internal/v1 ring, which requires an authenticated actor and carries the sensitive, per-customer operations. That ring is out of scope for a"},{"t":"The webhook event envelope","u":"/reference/webhook-event-envelope/","c":"API reference","e":"API reference","s":"Every webhook shares one envelope shape. The outer object identifies the event (id, type, created), flags whether it is live or test (livemode), pins the payload schema (api_version) and carries the resource under data.object. Routing on type and reading data.object is all most handlers ever need.","b":"Envelope fields FieldTypeNotesidstringUnique event id, evt_-prefixed. Use it to deduplicate.typestringDotted event type, e.g. decision.completed. Route on this.createdstringRFC 3339 UTC timestamp of when the event occurred.livemodebooleantrue for production, false for sandbox events.api_versionstringDate-versioned schema of data.object, e.g. 2026-07-01.data.objectobjectThe resource the event is about, in the shape of its API representation.data.previousobjectPresent on *.updated events: the changed fields’ prior values. Example { \"id\": \"evt_7Q2M9X\", \"type\": \"decision.completed\", \"created\": \"20"},{"t":"Token bucket","u":"/glossary/token-bucket/","c":"Developer glossary","e":"Glossary","s":"A token bucket meters traffic with a bucket that refills at a sustained rate and holds a burst capacity. Each request costs one token; the partner ring uses one per project.","b":"Definition A token bucket holds up to a burst number of tokens and refills continuously at a sustained rate. Each request spends one token; when the bucket is empty you are throttled until it refills. Because refill is continuous, you can spend a burst then settle to the sustained rate. In plain terms A rate limiter that lets you spike briefly but caps your steady pace. Why it matters here The partner ring meters per project this way (build 10 req/s, launch 25, scale 100). See rate limiting explained."},{"t":"Token introspection","u":"/glossary/token-introspection/","c":"Developer glossary","e":"Glossary","s":"Token introspection asks the authorization server whether a token is active and returns its scopes and expiry. Use it when you must honour a revocation immediately.","b":"Definition Introspection (RFC 7662) is a server call that returns active: true/false for a token plus its metadata. Unlike local JWKS verification, it sees revocations before natural expiry — at the cost of a round trip per check. In plain terms Asking the server, live, whether a token is still good. Why it matters here Reach for it on sensitive paths where instant revocation matters. See the introspection endpoint."},{"t":"Troubleshoot webhook signature failures","u":"/recipes/troubleshoot-webhook-signature-failures/","c":"Integration recipes","e":"Recipe","s":"Signature verification fails for four reasons, and only four. You verified against a parsed body instead of the raw bytes, used the wrong secret, hit clock skew beyond the 5-minute tolerance, or a proxy altered the request. Work through them in order.","b":"1. Are you using the raw body? The HMAC is over exact bytes. If a JSON middleware parsed and re-serialised the body first, the signature will never match. Capture the raw body before any parsing. 2. Right secret, right endpoint? Each endpoint has its own whsec_ secret, and test and live differ. A mismatch here fails every request. During a rotation, verify against both. 3. Clock skew The signed timestamp must be within 300 seconds of your server’s clock. If your host’s clock drifts, fresh deliveries look stale. Sync via NTP. 4. Proxy interference A load balancer or WAF that rewrites the body o"},{"t":"UTC timestamp","u":"/glossary/utc-timestamp/","c":"Developer glossary","e":"Glossary","s":"UTC timestamp — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is A UTC timestamp is a point in time expressed in Coordinated Universal Time, usually as an ISO-8601 string like 2026-07-04T12:00:00Z. The trailing Z means UTC. In the Credicorp API Public responses carry UTC timestamps — generated on the loyalty and biller endpoints, updated on CMS pages. Use them as cache-freshness markers. Convert to the user’s local zone only for display, never for cache logic. What is the Z at the end of the timestamp? It denotes UTC (Zulu time). The value is in Coordinated Universal Time; convert to a local zone only when displaying it."},{"t":"Validate inputs before you post","u":"/recipes/validate-inputs-before-you-post/","c":"Integration recipes","e":"Recipe","s":"Catch validation errors before the API does. The public ring enforces strict input rules server-side, but mirroring them in your client — mandatory consent, a valid email, a flat fields object within size limits — gives users instant feedback and saves a round trip. The server remains the final authority; your checks are a courtesy, not a substitute.","b":"The rules to mirror form — 1–64 chars, [a-z0-9_-].fields — flat object, ≤60 keys, ≤16 KiB encoded, values string/number/boolean only.fields.email — if present, a valid address.fields.consent — must equal \"yes\". Why mirror them Client-side validation gives immediate, friendly feedback and avoids a wasted request that would just come back as a 422. It improves the experience and reduces load. The server is still the authority Never trust the client. The API re-validates everything server-side and rejects anything that does not fit, regardless of what your form allowed. Treat your checks as UX, n"},{"t":"Verify a partner token locally with the JWKS","u":"/recipes/verify-a-partner-token-locally/","c":"Integration recipes","e":"Recipe","s":"Verify a partner JWT yourself instead of calling introspection every time: fetch the JWKS, cache by kid, and validate signature, issuer, audience and expiry. Re-fetch on an unknown kid.","b":"Fetch and cache the keys Pull the signing keys from GET /partner/v1/oauth/jwks and cache them keyed by kid. Do not fetch per request — that adds latency and burns quota. Refresh on a schedule and on demand. Validate the token For each incoming token, select the key by the JWT header's kid, verify the signature, then check the standard claims: iss matches the Credicorp issuer, aud matches your resource, and exp is in the future (allow a little leeway for clock skew). Reject on any failure. This is the local counterpart to introspection. Handle rotation and revocation On a token whose kid you do"},{"t":"Verify a webhook signature","u":"/recipes/verify-a-webhook-signature/","c":"Integration recipes","e":"Recipe","s":"Never trust an unverified webhook. Recompute the signature over the raw request body with your endpoint secret and compare in constant time; reject on mismatch before you do anything.","b":"Use the raw body Signatures are computed over the exact bytes Credicorp sent. Capture the raw request body before any JSON parsing or framework middleware reformats it — a re-serialised body will not match. Read the signature from the delivery header (see webhooks). Recompute and compare import hmac, hashlib expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, header_sig): return Response(status=400) # reject — do not processUse a constant-time comparison (hmac.compare_digest) so an attacker cannot learn the signature byte by byte from timing. "},{"t":"Versioning and deprecation policy","u":"/public-api/versioning-and-deprecation/","c":"Public API guides","e":"Reliability","s":"Both rings are versioned in the path (/public/v1, /partner/v1). Additive changes ship within a version; breaking changes get a new version and a deprecation window with advance notice on the changelog.","b":"What version means here The v1 in /public/v1 and /partner/v1 is the major version. Within a major version, changes are additive: new fields, new endpoints, new optional parameters. Your integration should ignore unknown JSON fields it does not use, so an additive change never breaks you. A change that would break existing clients — removing a field, renaming one, tightening a type — waits for a new major version rather than shipping into v1. How deprecations are signalled When something is scheduled for removal it is announced on the developer changelog ahead of time, with a deprecation window"},{"t":"Versioning and stability of the public API","u":"/public-api/versioning-and-stability/","c":"Public API guides","e":"Public API","s":"The v1 in /public/v1 is a stability contract. Within a major version the platform adds fields and endpoints but does not remove or rename what you already depend on, and it never changes the meaning of an existing field. Breaking changes would arrive under a new version prefix, not silently inside v1.","b":"What v1 guarantees The version is in the path so it is explicit and unmissable. Inside v1, changes are additive: new endpoints and new response fields may appear, but existing fields keep their name, type and meaning. That means a well-written integration keeps working as the platform grows — as long as you ignore fields you do not recognise rather than rejecting them. Write tolerant parsers Follow the robustness principle: be liberal in what you accept. Do not fail if a response contains a field you have never seen; do not assume the order of array elements beyond what the docs promise (the l"},{"t":"Webhook","u":"/glossary/webhook/","c":"Developer glossary","e":"Glossary","s":"A webhook is a signed HTTP POST Credicorp sends to your URL when an event happens — a decision, a settlement, a signature. Delivery is at-least-once; verify before acting.","b":"Definition A webhook is an outbound callback: instead of you polling, Credicorp POSTs a signed event to your registered endpoint when something changes. Delivery is at-least-once, so duplicates are normal, and each carries a signature you must verify. In plain terms A push notification from the API to your server when an event occurs. Why it matters here Verify the signature, respond 2xx fast, and process idempotently. See webhooks and event delivery."},{"t":"Webhook","u":"/glossary/webhook-callbacks/","c":"Developer glossary","e":"Glossary","s":"Webhook — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is A webhook is an HTTP callback: instead of you polling for changes, the platform posts an event to a URL you register when something happens. In the Credicorp API Webhooks are an authenticated partner feature — they carry event data and must be secured and verified. They are not part of the unauthenticated public ring. If your integration needs event callbacks, see the partner webhooks docs and moving to the partner API. Can I receive webhooks from the public API? No. Webhooks are an authenticated partner-API feature. The public ring is request/response only; there are no callbacks."},{"t":"Webhook API versioning","u":"/reference/webhook-api-versioning/","c":"API reference","e":"API reference","s":"Webhook payloads are versioned by date. Each event carries api_version, e.g. 2026-07-01. Within a version, changes are only ever additive — new fields, new event types. Anything that could break a parser (removing a field, changing a type) ships as a new version you opt into deliberately.","b":"What \"additive\" means Inside one api_version you may see new fields appear on data.object and new event types in the catalogue. You will never see an existing field removed, renamed or change type. Parse defensively — read the fields you need, ignore the rest — and additive change never breaks you. Pinning and upgrading An endpoint is pinned to the API version current when it was created. Upgrade explicitly once you have tested the new payloads against a sandbox endpoint. Because upgrades are opt-in, a new version can never silently reshape production traffic. What happens if I never upgrade? "},{"t":"Webhook delivery","u":"/glossary/webhook-delivery/","c":"Developer glossary","e":"Glossary","s":"Webhook delivery is the act of Credicorp POSTing an event to your endpoint and waiting for a 2xx acknowledgement within 10 seconds, retrying on backoff if it does not arrive.","b":"Definition Webhook delivery covers everything from the moment an event fires to the moment your endpoint acknowledges it. Credicorp sends the signed envelope, expects a 2xx within a 10-second window, and — if it does not get one — retries on exponential backoff for 72 hours. Delivery is at-least-once and unordered. What acknowledges a delivery? Any `2xx` status within 10 seconds. The response body is ignored. See [Webhook delivery and retries](/reference/webhook-delivery-and-retries/)."},{"t":"Webhook delivery and retries","u":"/reference/webhook-delivery-and-retries/","c":"API reference","e":"API reference","s":"Credicorp expects a 2xx within 10 seconds. Return one to acknowledge, then process asynchronously. Any non-2xx or timeout triggers a retry on exponential backoff — roughly a dozen attempts spread over 72 hours. Delivery is at-least-once, so build for redelivery. After 7 consecutive days of failure an endpoint is disabled.","b":"The acknowledgement contract Any 2xx status means \"received\" — the body is ignored. Do the minimum synchronously: verify the signature, enqueue the event, return 200. If you run business logic inline and it exceeds the 10-second timeout, the delivery is marked failed and retried even though you processed it, which is why idempotency is not optional. Retry schedule Failed deliveries back off exponentially. The schedule is approximately:AttemptDelay after previousElapsed1— (immediate)02~1 min~1 min3~5 min~6 min4~30 min~36 min5~2 h~2.5 h6–12~6–12 h eachup to 72 hRetries stop once the endpoint ret"},{"t":"Webhook event catalogue","u":"/reference/webhook-event-catalogue/","c":"API reference","e":"API reference","s":"This is the master list of webhook type values. Events are grouped by resource — enquiry, decision, loan, payment, account, consent — with a consistent resource.verb naming. Subscribe an <a href=\"/reference/register-a-webhook-endpoint/\">endpoint</a> to the specific types you handle, or for all of them.","b":"All event types TypeFires whendata.objectenquiry.createdA public enquiry is submittedenquiryenquiry.updatedAn enquiry’s status changesenquirydecision.completedA lending decision is reacheddecisiondecision.referredA decision is referred for manual reviewdecisionloan.activatedA loan is drawn down / goes liveloanloan.closedA loan is fully repaid or written offloanpayment.succeededA scheduled repayment settlespaymentpayment.failedA repayment attempt failspaymentaccount.updatedA customer account record changesaccountconsent.grantedA consent flag is setconsentconsent.withdrawnA consent flag is withd"},{"t":"Webhook event: account.updated","u":"/reference/event-account-updated/","c":"API reference","e":"API reference","s":"The account.updated webhook fires when a customer account record changes. Its data.object is a account. A typical handler will sync the change into your own account record once the signature is verified. Because delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once</a>, process it <a href=\"/glossary/idempotency-explained/\">idempotently</a> on the event id.","b":"When it fires account.updated is emitted when a customer account record changes. It is an *.updated event, so data.previous carries the fields’ prior values.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_ACCO7X\", \"type\": \"account.updated\", \"created\": \"2026-07-04T10:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"acct_4B8N2\", \"status\": \"active"},{"t":"Webhook event: consent.granted","u":"/reference/event-consent-granted/","c":"API reference","e":"API reference","s":"The consent.granted webhook fires when a consent flag is set. Its data.object is a consent. A typical handler will unlock the permitted communication channel once the signature is verified. Because delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once</a>, process it <a href=\"/glossary/idempotency-explained/\">idempotently</a> on the event id.","b":"When it fires consent.granted is emitted when a consent flag is set. It fires once per occurrence; a redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_CONS7X\", \"type\": \"consent.granted\", \"created\": \"2026-07-04T10:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"con_1A5C9\", \"subject\": \"en"},{"t":"Webhook event: consent.withdrawn","u":"/reference/event-consent-withdrawn/","c":"API reference","e":"API reference","s":"The consent.withdrawn webhook fires when a consent flag is withdrawn. Its data.object is a consent. A typical handler will immediately suppress that channel once the signature is verified. Because delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once</a>, process it <a href=\"/glossary/idempotency-explained/\">idempotently</a> on the event id.","b":"When it fires consent.withdrawn is emitted when a consent flag is withdrawn. It fires once per occurrence; a redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_CONS7X\", \"type\": \"consent.withdrawn\", \"created\": \"2026-07-04T10:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"con_1A5C9\", \"sub"},{"t":"Webhook event: decision.completed","u":"/reference/event-decision-completed/","c":"API reference","e":"API reference","s":"The decision.completed webhook fires when a lending decision is reached. Its data.object is a decision. A typical handler will advance the customer to the offer or decline screen once the signature is verified. Because delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once</a>, process it <a href=\"/glossary/idempotency-explained/\">idempotently</a> on the event id.","b":"When it fires decision.completed is emitted when a lending decision is reached. It fires once per occurrence; a redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_DECI7X\", \"type\": \"decision.completed\", \"created\": \"2026-07-04T10:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"dec_5H2N8\", "},{"t":"Webhook event: decision.referred","u":"/reference/event-decision-referred/","c":"API reference","e":"API reference","s":"The decision.referred webhook fires when a decision is referred for manual review. Its data.object is a decision. A typical handler will show a \"we’re reviewing this\" state once the signature is verified. Because delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once</a>, process it <a href=\"/glossary/idempotency-explained/\">idempotently</a> on the event id.","b":"When it fires decision.referred is emitted when a decision is referred for manual review. It fires once per occurrence; a redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_DECI7X\", \"type\": \"decision.referred\", \"created\": \"2026-07-04T10:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"dec"},{"t":"Webhook event: enquiry.created","u":"/reference/event-enquiry-created/","c":"API reference","e":"API reference","s":"The enquiry.created webhook fires when a public enquiry is submitted. Its data.object is a enquiry. A typical handler will route it to your CRM or ticketing system once the signature is verified. Because delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once</a>, process it <a href=\"/glossary/idempotency-explained/\">idempotently</a> on the event id.","b":"When it fires enquiry.created is emitted when a public enquiry is submitted. It fires once per occurrence; a redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_ENQU7X\", \"type\": \"enquiry.created\", \"created\": \"2026-07-04T09:15:22Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"enq_9F3K2P\", \"form"},{"t":"Webhook event: enquiry.updated","u":"/reference/event-enquiry-updated/","c":"API reference","e":"API reference","s":"The enquiry.updated webhook fires when an enquiry’s status changes (e.g. new → in_progress). Its data.object is a enquiry. A typical handler will update the mirrored record in your system once the signature is verified. Because delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once</a>, process it <a href=\"/glossary/idempotency-explained/\">idempotently</a> on the event id.","b":"When it fires enquiry.updated is emitted when an enquiry’s status changes (e.g. new → in_progress). It is an *.updated event, so data.previous carries the fields’ prior values.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_ENQU7X\", \"type\": \"enquiry.updated\", \"created\": \"2026-07-04T10:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"enq_9F3K2P\""},{"t":"Webhook event: kyc.passed","u":"/reference/event-kyc-passed/","c":"API reference","e":"API reference","s":"The kyc.passed webhook fires when identity and business verification passes. Its data.object is a kyc. A typical handler will advance the application to decisioning after verifying the signature. Delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once and unordered</a>, so handle it <a href=\"/glossary/idempotency-explained/\">idempotently</a>.","b":"When it fires kyc.passed is emitted when identity and business verification passes. A redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_KYCP9Z\", \"type\": \"kyc.passed\", \"created\": \"2026-07-05T12:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"kyc_9D2F6\", \"subject\": \"acct_4B8N2\", \"outcome\""},{"t":"Webhook event: kyc.referred","u":"/reference/event-kyc-referred/","c":"API reference","e":"API reference","s":"The kyc.referred webhook fires when verification is referred for manual checks. Its data.object is a kyc. A typical handler will show a \"checks in progress\" state after verifying the signature. Delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once and unordered</a>, so handle it <a href=\"/glossary/idempotency-explained/\">idempotently</a>.","b":"When it fires kyc.referred is emitted when verification is referred for manual checks. A redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_KYCR9Z\", \"type\": \"kyc.referred\", \"created\": \"2026-07-05T12:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"kyc_9D2F6\", \"subject\": \"acct_4B8N2\", \"out"},{"t":"Webhook event: loan.activated","u":"/reference/event-loan-activated/","c":"API reference","e":"API reference","s":"The loan.activated webhook fires when a loan is drawn down and becomes live. Its data.object is a loan. A typical handler will kick off your onboarding or fulfilment flow once the signature is verified. Because delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once</a>, process it <a href=\"/glossary/idempotency-explained/\">idempotently</a> on the event id.","b":"When it fires loan.activated is emitted when a loan is drawn down and becomes live. It fires once per occurrence; a redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_LOAN7X\", \"type\": \"loan.activated\", \"created\": \"2026-07-04T10:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"loan_2K9P4\","},{"t":"Webhook event: loan.arrears","u":"/reference/event-loan-arrears/","c":"API reference","e":"API reference","s":"The loan.arrears webhook fires when a loan falls into arrears. Its data.object is a loan. A typical handler will trigger a supportive, hardship-aware outreach after verifying the signature. Delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once and unordered</a>, so handle it <a href=\"/glossary/idempotency-explained/\">idempotently</a>.","b":"When it fires loan.arrears is emitted when a loan falls into arrears. A redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_LOAN9Z\", \"type\": \"loan.arrears\", \"created\": \"2026-07-05T12:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"loan_2K9P4\", \"status\": \"arrears\", \"days_past_due\": 4, \"arr"},{"t":"Webhook event: loan.closed","u":"/reference/event-loan-closed/","c":"API reference","e":"API reference","s":"The loan.closed webhook fires when a loan is fully repaid or written off. Its data.object is a loan. A typical handler will archive the account and stop reminders once the signature is verified. Because delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once</a>, process it <a href=\"/glossary/idempotency-explained/\">idempotently</a> on the event id.","b":"When it fires loan.closed is emitted when a loan is fully repaid or written off. It fires once per occurrence; a redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_LOAN7X\", \"type\": \"loan.closed\", \"created\": \"2026-07-04T10:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"loan_2K9P4\", \"stat"},{"t":"Webhook event: loan.disbursed","u":"/reference/event-loan-disbursed/","c":"API reference","e":"API reference","s":"The loan.disbursed webhook fires when loan funds are sent to the customer. Its data.object is a loan. A typical handler will confirm funding to the customer after verifying the signature. Delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once and unordered</a>, so handle it <a href=\"/glossary/idempotency-explained/\">idempotently</a>.","b":"When it fires loan.disbursed is emitted when loan funds are sent to the customer. A redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_LOAN9Z\", \"type\": \"loan.disbursed\", \"created\": \"2026-07-05T12:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"loan_2K9P4\", \"disbursed_amount\": 25000, \"cur"},{"t":"Webhook event: loan.restructured","u":"/reference/event-loan-restructured/","c":"API reference","e":"API reference","s":"The loan.restructured webhook fires when a loan’s terms are restructured (e.g. a payment plan). Its data.object is a loan. A typical handler will update the displayed schedule and balance after verifying the signature. Delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once and unordered</a>, so handle it <a href=\"/glossary/idempotency-explained/\">idempotently</a>.","b":"When it fires loan.restructured is emitted when a loan’s terms are restructured (e.g. a payment plan). A redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_LOAN9Z\", \"type\": \"loan.restructured\", \"created\": \"2026-07-05T12:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"loan_2K9P4\", \"status"},{"t":"Webhook event: loyalty.tier_changed","u":"/reference/event-loyalty-tier-changed/","c":"API reference","e":"API reference","s":"The loyalty.tier_changed webhook fires when a customer moves loyalty tier. Its data.object is a loyalty. A typical handler will refresh any tier-dependent pricing or perks you show after verifying the signature. Delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once and unordered</a>, so handle it <a href=\"/glossary/idempotency-explained/\">idempotently</a>.","b":"When it fires loyalty.tier_changed is emitted when a customer moves loyalty tier. It is a change event, so data.previous carries the prior values of the changed fields.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_LOYA9Z\", \"type\": \"loyalty.tier_changed\", \"created\": \"2026-07-05T12:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"acct_4B8N2\", \""},{"t":"Webhook event: mandate.activated","u":"/reference/event-mandate-activated/","c":"API reference","e":"API reference","s":"The mandate.activated webhook fires when a direct debit / payment mandate becomes active. Its data.object is a mandate. A typical handler will confirm the mandate is set up after verifying the signature. Delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once and unordered</a>, so handle it <a href=\"/glossary/idempotency-explained/\">idempotently</a>.","b":"When it fires mandate.activated is emitted when a direct debit / payment mandate becomes active. A redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_MAND9Z\", \"type\": \"mandate.activated\", \"created\": \"2026-07-05T12:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"mnd_3C7L9\", \"loan_id\": \"lo"},{"t":"Webhook event: mandate.cancelled","u":"/reference/event-mandate-cancelled/","c":"API reference","e":"API reference","s":"The mandate.cancelled webhook fires when a payment mandate is cancelled. Its data.object is a mandate. A typical handler will prompt the customer to set up a new mandate after verifying the signature. Delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once and unordered</a>, so handle it <a href=\"/glossary/idempotency-explained/\">idempotently</a>.","b":"When it fires mandate.cancelled is emitted when a payment mandate is cancelled. A redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_MAND9Z\", \"type\": \"mandate.cancelled\", \"created\": \"2026-07-05T12:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"mnd_3C7L9\", \"status\": \"cancelled\", \"cancell"},{"t":"Webhook event: offer.accepted","u":"/reference/event-offer-accepted/","c":"API reference","e":"API reference","s":"The offer.accepted webhook fires when a customer accepts a loan offer. Its data.object is a offer. A typical handler will move the customer into onboarding after verifying the signature. Delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once and unordered</a>, so handle it <a href=\"/glossary/idempotency-explained/\">idempotently</a>.","b":"When it fires offer.accepted is emitted when a customer accepts a loan offer. A redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_OFFE9Z\", \"type\": \"offer.accepted\", \"created\": \"2026-07-05T12:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"off_6P1K3\", \"status\": \"accepted\", \"accepted_at\":"},{"t":"Webhook event: offer.created","u":"/reference/event-offer-created/","c":"API reference","e":"API reference","s":"The offer.created webhook fires when a loan offer is generated after approval. Its data.object is a offer. A typical handler will present the offer to the customer after verifying the signature. Delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once and unordered</a>, so handle it <a href=\"/glossary/idempotency-explained/\">idempotently</a>.","b":"When it fires offer.created is emitted when a loan offer is generated after approval. A redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_OFFE9Z\", \"type\": \"offer.created\", \"created\": \"2026-07-05T12:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"off_6P1K3\", \"decision_id\": \"dec_5H2N8\", \""},{"t":"Webhook event: offer.expired","u":"/reference/event-offer-expired/","c":"API reference","e":"API reference","s":"The offer.expired webhook fires when a loan offer lapses without acceptance. Its data.object is a offer. A typical handler will show a \"start again\" prompt after verifying the signature. Delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once and unordered</a>, so handle it <a href=\"/glossary/idempotency-explained/\">idempotently</a>.","b":"When it fires offer.expired is emitted when a loan offer lapses without acceptance. A redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_OFFE9Z\", \"type\": \"offer.expired\", \"created\": \"2026-07-05T12:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"off_6P1K3\", \"status\": \"expired\" } } } Handl"},{"t":"Webhook event: payment.disputed","u":"/reference/event-payment-disputed/","c":"API reference","e":"API reference","s":"The payment.disputed webhook fires when a payment is disputed (e.g. an indemnity claim). Its data.object is a payment. A typical handler will flag the account for review and pause dunning after verifying the signature. Delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once and unordered</a>, so handle it <a href=\"/glossary/idempotency-explained/\">idempotently</a>.","b":"When it fires payment.disputed is emitted when a payment is disputed (e.g. an indemnity claim). A redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_PAYM9Z\", \"type\": \"payment.disputed\", \"created\": \"2026-07-05T12:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"pay_7M3X1\", \"status\": \"dispu"},{"t":"Webhook event: payment.failed","u":"/reference/event-payment-failed/","c":"API reference","e":"API reference","s":"The payment.failed webhook fires when a repayment attempt fails. Its data.object is a payment. A typical handler will trigger a dunning or hardship-signposting flow once the signature is verified. Because delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once</a>, process it <a href=\"/glossary/idempotency-explained/\">idempotently</a> on the event id.","b":"When it fires payment.failed is emitted when a repayment attempt fails. It fires once per occurrence; a redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_PAYM7X\", \"type\": \"payment.failed\", \"created\": \"2026-07-04T10:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"pay_7M3X1\", \"loan_id\": \""},{"t":"Webhook event: payment.refunded","u":"/reference/event-payment-refunded/","c":"API reference","e":"API reference","s":"The payment.refunded webhook fires when a settled payment is refunded. Its data.object is a payment. A typical handler will reconcile the refund against the balance after verifying the signature. Delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once and unordered</a>, so handle it <a href=\"/glossary/idempotency-explained/\">idempotently</a>.","b":"When it fires payment.refunded is emitted when a settled payment is refunded. A redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_PAYM9Z\", \"type\": \"payment.refunded\", \"created\": \"2026-07-05T12:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"pay_7M3X1\", \"loan_id\": \"loan_2K9P4\", \"refund_a"},{"t":"Webhook event: payment.succeeded","u":"/reference/event-payment-succeeded/","c":"API reference","e":"API reference","s":"The payment.succeeded webhook fires when a scheduled repayment settles. Its data.object is a payment. A typical handler will reconcile the payment and update the balance once the signature is verified. Because delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once</a>, process it <a href=\"/glossary/idempotency-explained/\">idempotently</a> on the event id.","b":"When it fires payment.succeeded is emitted when a scheduled repayment settles. It fires once per occurrence; a redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_PAYM7X\", \"type\": \"payment.succeeded\", \"created\": \"2026-08-04T02:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"pay_7M3X1\", \"l"},{"t":"Webhook event: statement.generated","u":"/reference/event-statement-generated/","c":"API reference","e":"API reference","s":"The statement.generated webhook fires when a monthly statement is produced. Its data.object is a statement. A typical handler will notify the customer their statement is ready after verifying the signature. Delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once and unordered</a>, so handle it <a href=\"/glossary/idempotency-explained/\">idempotently</a>.","b":"When it fires statement.generated is emitted when a monthly statement is produced. A redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_STAT9Z\", \"type\": \"statement.generated\", \"created\": \"2026-07-05T12:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"stmt_8N4B2\", \"loan_id\": \"loan_2K9P4\", "},{"t":"Webhook event: support.chat_escalated","u":"/reference/event-support-chat-escalated/","c":"API reference","e":"API reference","s":"The support.chat_escalated webhook fires when a support chat is escalated to a human. Its data.object is a chat. A typical handler will notify your support queue if you mirror chats after verifying the signature. Delivery is <a href=\"/reference/webhook-delivery-and-retries/\">at-least-once and unordered</a>, so handle it <a href=\"/glossary/idempotency-explained/\">idempotently</a>.","b":"When it fires support.chat_escalated is emitted when a support chat is escalated to a human. A redelivery of the same occurrence carries the same event id.If your endpoint does not return a 2xx within the timeout, delivery is retried on an exponential backoff schedule — see Webhook delivery and retries. Design your handler to be idempotent so a redelivered event is safe to process twice. Example delivery { \"id\": \"evt_SUPP9Z\", \"type\": \"support.chat_escalated\", \"created\": \"2026-07-05T12:00:00Z\", \"livemode\": true, \"api_version\": \"2026-07-01\", \"data\": { \"object\": { \"id\": \"chat_5K8M1\", \"reason\": \"c"},{"t":"Webhook signature verification","u":"/reference/webhook-signature-verification/","c":"API reference","e":"API reference","s":"Verify every webhook before you trust it. Each delivery carries a Credicorp-Signature header: a timestamp and an HMAC-SHA256 of timestamp.rawbody, keyed by your signing secret. Recompute it, compare in constant time, and reject anything older than the 5-minute tolerance. Never parse the body until the signature checks out.","b":"The signature header Each request includes:Credicorp-Signature: t=1751619922,v1=5f8d1c…t is the Unix timestamp the signature was generated. v1 is the hex HMAC-SHA256 of the string {t}.{raw_request_body}, keyed by your endpoint’s whsec_ signing secret. Multiple v1= values may appear during a secret rotation; accept the request if any one matches. Verify in Node const crypto = require('crypto'); function verify(rawBody, header, secret) { const parts = Object.fromEntries(header.split(',').map(p => p.split('='))); const signed = `${parts.t}.${rawBody}`; const expected = crypto.createHmac('sha256',"},{"t":"Webhooks and event delivery","u":"/public-api/webhooks-and-event-delivery/","c":"Public API guides","e":"Partner API","s":"Credicorp pushes events — decision made, payment settled, document signed — to your registered URL as signed POSTs. Verify the signature, respond 2xx fast, and process idempotently because delivery is at-least-once.","b":"How delivery works When something happens on a partner resource you own — a decision is reached, a payment settles, an e-sign envelope completes — Credicorp POSTs a JSON event to the URL you registered. Each delivery carries a signature header derived from a shared secret so you can prove the payload came from Credicorp and was not tampered with. Your endpoint should verify the signature, do minimal synchronous work, and return a 2xx quickly; queue the heavy processing. Retries and ordering Delivery is at-least-once. If your endpoint is slow, errors, or returns a non-2xx, Credicorp retries on "},{"t":"Webhooks explained","u":"/public-api/webhooks-explained/","c":"Public API guides","e":"Public API","s":"A webhook is an HTTP callback Credicorp sends to your server when something happens — an enquiry is received, a decision is reached, a payment settles. Instead of polling the API on a timer, you register one HTTPS endpoint and we push a signed JSON event to it as events occur. Delivery is at-least-once, every request carries an HMAC-SHA256 signature, and failed deliveries are retried on exponential backoff for up to 72 hours.","b":"Why webhooks Polling wastes both sides’ resources and adds latency: you either poll too often (and hit the rate limit) or too rarely (and learn about a decision minutes late). A webhook inverts the flow. You tell Credicorp where to reach you once, and we deliver each event within seconds of it happening.Webhooks are delivered from the Credicorp platform to your HTTPS endpoint. Unlike the read-only /public/v1 ring, a webhook is an outbound push: Credicorp is the client and your server is the origin. Every request is signed so you can prove it came from us — see Webhook signature verification. T"},{"t":"What you can build without a token","u":"/public-api/what-you-can-build-without-a-token/","c":"Public API guides","e":"Public API","s":"You can ship real products with no token at all: a live quote widget, a comparison listing, a loyalty display, and a fully-grounded AI assistant — every one from the public ring alone.","b":"Front-end and content A live quote widget, a comparison listing and a loyalty-ladder display are all pure public reads. They render accurate, current Credicorp figures with no credential and hand qualified users to apply. AI and agents The public MCP server lets you build a grounded assistant — qualify, quote, guide — with no token. This is the highest-leverage token-free build: an agent that answers accurately about Credicorp because it reads live tools rather than guessing. When you finally need a token The moment you want to take the application yourself (rather than hand off), read a decis"},{"t":"Why the figures reconcile across the estate","u":"/public-api/figures-reconcile-across-the-estate/","c":"Public API guides","e":"Public API","s":"Every Credicorp figure — on the site, in the API, through the MCP tools — comes from one source: the pricing config. That single-sourcing is why the numbers never disagree, and why you should read them, not hard-code them.","b":"The single source Credicorp's published figures live in one place, exposed at config/pricing. The marketing estate, the on-page calculators, the quote engine and the MCP tools all read from it. Change the config and every surface updates together — there is no second copy to drift. What this means for you Read figures from the API rather than hard-coding them in your integration. Then a Credicorp pricing change flows to you on your next fetch, with zero code change on your side, and your product never shows a number that disagrees with the Credicorp site. Hard-coded figures are the one reliabl"},{"t":"curl","u":"/glossary/curl/","c":"Developer glossary","e":"Glossary","s":"curl — a term used across the Credicorp developer documentation, defined here for engineers integrating the public /public/v1 API.","b":"What it is curl is a ubiquitous command-line tool for making HTTP requests. It is the reference client in these docs because it maps one-to-one onto the raw request. In the Credicorp API Every example on the reference pages is a curl command you can paste and run. A read is a bare curl; a write adds -X POST, a Content-Type header and a -d body. Start with your first call. Do I have to use curl? No — any HTTP client works. curl is just the clearest way to show the raw request in documentation."},{"t":"error.param","u":"/glossary/error-param/","c":"Developer glossary","e":"Glossary","s":"error.param is the envelope field that names the exact input which failed validation — present on field-level errors like a bad email or a missing consent flag.","b":"Definition error.param appears in the error envelope whenever a failure is about one specific input. On a 422, it points straight at the offending field — fields.consent, fields.email — so you can highlight it in your form. When an error is not field-specific (a rate limit, a server error) it is absent. Is error.param always present? No — only on field-validation errors. Rate limits and server errors have no single offending field, so they omit it."},{"t":"tools/call (MCP)","u":"/glossary/tools-call/","c":"Developer glossary","e":"Glossary","s":"tools/call is the MCP method that invokes a tool by name with arguments. It is how an agent actually runs get_quote or any other Credicorp tool and gets a result back.","b":"Definition In MCP, an agent discovers tools with tools/list and runs one with tools/call, passing the tool name and an arguments object matching the tool's input schema. The result (or a JSON-RPC error like -32602 for bad params) comes back in the response. In plain terms The command that says 'run this tool with these inputs'. Why it matters here It is the workhorse call against the MCP server. See get_quote and the JSON-RPC recipe."}]}