Authentication

OAuth 2.0

Server-to-server calls to the Credit Corp business-lending API use the OAuth 2.0 client-credentials grant. End-user sign-in — “Sign in with Credit Corp” — uses OpenID Connect on top of the same authorization server.

Two flows, two servers. Your backend authenticates with client-credentials on the partner OAuth server at hub.credicorp.co.uk to call the API on its own behalf. To identify a director or signatory in your own UI, use the OpenID Connect authorization-code flow served by the SSO server at sso.credicorp.co.uk.

Client-credentials grant

Exchange your project's client_id and client_secret for a short-lived bearer token. Request only the scopes the call needs — a token minted for mcp.read alone cannot reach account.read data. The token endpoint accepts application/x-www-form-urlencoded or HTTP Basic for the credentials.

POSThttps://hub.credicorp.co.uk/partner/v1/oauth/token
bash
curl -s https://hub.credicorp.co.uk/partner/v1/oauth/token \
  -d "grant_type=client_credentials" \
  -d "client_id=$CC_CLIENT_ID" \
  -d "client_secret=$CC_CLIENT_SECRET" \
  -d "scope=mcp.read account.read"
javascript
import { Credit Corp } from "@credicorp/sdk";

// The SDK fetches, caches and refreshes the token for you.
const cc = new Credit Corp({
  clientId: process.env.CC_CLIENT_ID,
  clientSecret: process.env.CC_CLIENT_SECRET,
  scopes: ["mcp.read", "account.read"],
});

const token = await cc.auth.accessToken();
php
use Credit Corp\Client;

// Token acquisition and refresh are handled by the client.
$cc = new Client([
    'client_id'     => getenv('CC_CLIENT_ID'),
    'client_secret' => getenv('CC_CLIENT_SECRET'),
    'scopes'        => ['mcp.read', 'account.read'],
]);

$token = $cc->auth()->accessToken();

Request parameters

FieldTypeDescription
grant_typestringreqAlways client_credentials.
client_idstringreqProject client identifier. Begins cc_client_.
client_secretstringreqProject secret. Server-side only — never ship it to a browser or mobile app.
scopestringoptSpace-separated scopes. Defaults to the project's full granted set; narrow it to follow least-privilege.
audiencestringoptTarget API. Defaults to partner/v1. Set internal/v1 only if your project is provisioned for it.

Token response

json
# → 200 OK
{
  "access_token": "eyJhbGciOiJFZERTQSIsImtpZCI6Imh1Yi1vYXV0aC1lZDI1NTE5In0…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "mcp.read account.read"
}

The access token is a signed EdDSA JWT (Ed25519, key id hub-oauth-ed25519). You don't need to parse it — treat it as an opaque bearer string — but if you do, the public keys are published at the JWKS endpoint below so you can verify iss, aud, exp and scope locally.

Scope catalogue

The partner OAuth plane is read-only. All four scopes use the .read dot form (distinct from the internal colon form) so they can never be confused with write grants. A request that touches data outside its token's scope is rejected with 403 insufficient_scope and a WWW-Authenticate header naming the scope required.

ScopeTierGrants
mcp.readFloorAuthed MCP handshake, tool list and decisioning explanation. The minimum grant — required by every partner token.
account.readStaffPII reads: application status, customer summary, loan status, loan statement and behavioural digest. Every call is audited.
ops.readStaffOperational reads: queue status and enquiry list (non-PII aggregates).
owner.readOwnerPlatform metrics, ops metrics, portfolio overview and config snapshot. The strongest standing partner grant.

Least privilege. An MCP client that only needs tool discovery should request mcp.read alone. Add account.read only if your integration reads customer or loan data — the extra scope triggers a mandatory audit trail on every call.

Token caching

Access tokens are valid for one hour (expires_in: 3600). Cache and reuse them. Minting a fresh token on every API call is the most common integration mistake — it doubles your request volume and will trip the 10 req/s limit on the token endpoint.

  • Store the token in process memory, or in Redis / Memcached if you run multiple instances, keyed by client_id + scope-set.
  • Refresh proactively at ~90% of lifetime (roughly 60 seconds before exp) so an in-flight request never races expiry.
  • On a 401 invalid_token, refresh once and retry the original request a single time.
javascript
let cached = { token: null, exp: 0 };

async function getToken() {
  const now = Date.now() / 1000;
  if (cached.token && now < cached.exp - 60) return cached.token;   // reuse

  const r = await fetch("https://hub.credicorp.co.uk/partner/v1/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "client_credentials",
      client_id: process.env.CC_CLIENT_ID,
      client_secret: process.env.CC_CLIENT_SECRET,
      scope: "mcp.read account.read",
    }),
  });
  const j = await r.json();
  cached = { token: j.access_token, exp: now + j.expires_in };
  return cached.token;
}

The official SDKs do all of this for you — in-memory caching, proactive refresh and one-shot retry on 401. Reach for raw token handling only if you can't use an SDK.

Rotating client secrets

The authorization server supports two live secrets per client so you can rotate with zero downtime. The roll never requires a maintenance window.

  1. Generate a second secret from the developer dashboard (or POST /oauth/clients/{id}/secrets). Both old and new now authenticate.
  2. Deploy the new secret to your environment — rolling restart, blue/green, whatever you run. Old instances keep working on the old secret.
  3. Verify traffic is minting tokens with the new secret (the dashboard shows last-used timestamps per secret).
  4. Revoke the old secret. Any token already issued under it stays valid until it expires; no new tokens can be minted with it.

If a secret leaks, revoke immediately — don't wait for the orderly roll. Revocation is instant. Then issue a fresh secret and redeploy. You can also revoke all outstanding access tokens for a client from the dashboard, which forces every cache to re-mint.

Authorization-server discovery

Never hard-code endpoint URLs. Fetch the RFC 8414 authorization-server metadata document at start-up and cache the endpoint addresses. The document is published at:

GEThttps://hub.credicorp.co.uk/.well-known/oauth-authorization-server
json
{
  "issuer": "https://hub.credicorp.co.uk",
  "token_endpoint": "https://hub.credicorp.co.uk/partner/v1/oauth/token",
  "jwks_uri": "https://hub.credicorp.co.uk/partner/v1/oauth/jwks",
  "introspection_endpoint": "https://hub.credicorp.co.uk/partner/v1/oauth/introspect",
  "grant_types_supported": ["client_credentials"],
  "response_types_supported": [],
  "scopes_supported": ["mcp.read", "account.read", "ops.read", "owner.read"],
  "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
  "id_token_signing_alg_values_supported": ["EdDSA"],
  "service_documentation": "https://hub.credicorp.co.uk/partner/v1/auth.md"
}

Access tokens are EdDSA JWTs (Ed25519, kid=hub-oauth-ed25519). Verify the signature against the JWKS published at jwks_uri and check iss, aud, exp and scope. The partner plane supports only client_credentials — there is no authorization-code or refresh-token flow.

Authorization-server endpoints

MethodEndpointPurpose
POST/partner/v1/oauth/tokenIssue an access token (client-credentials grant).
POST/partner/v1/oauth/introspectInspect a token's active state, scope and expiry.
GET/partner/v1/oauth/jwksEdDSA public key (JWKS) for token verification.
GET/.well-known/oauth-authorization-serverRFC 8414 authorization-server metadata.

Token errors

The token endpoint returns standard OAuth 2.0 errors. Treat invalid_client as a hard failure — do not retry with the same credentials.

StatusErrorCause
400invalid_requestMissing or malformed grant_type / parameters.
401invalid_clientUnknown client_id or wrong client_secret. Check you're not mixing sandbox and live.
400invalid_scopeRequested a scope the project isn't granted.
403insufficient_scopeReturned by the API when a call needs a scope your token lacks.
503temporarily_unavailableAuth server briefly unavailable — back off and retry.

Most integrations only need client-credentials. If you're calling from a browser, mobile app or webhook receiver instead of your own backend, read API keys for publishable keys and webhook secrets, and Request signing to verify webhook deliveries.