Errors
Every error from the Credicorp API uses one envelope and a conventional HTTP status. Read the machine-readable code to branch your logic and the message for logs — never parse the prose. Quote the correlation_id when you contact support.
The API uses standard HTTP status codes: 2xx for success, 4xx when something about the request is wrong (and retrying unchanged won't help), and 5xx for the rare problem on our side. Failures relating to lending outcomes — a declined decision, a returned payment — are not API errors: those succeed with 2xx and carry the outcome in the resource body.
The error envelope
All errors share this shape. The top-level key is always error.
{
"error": {
"code": "validation_failed",
"message": "The request failed validation.",
"detail": [{ "field": "amount_pence", "reason": "required" }],
"correlation_id": "cor_01J2K3M4N5P6Q7R8S9T0A1V2W3",
"retryable": false
}
}| Field | Type | Description | |
|---|---|---|---|
code | string | always | Stable, machine-readable identifier for the specific error. Switch on this in your error handler. |
message | string | always | Human-readable explanation for logs. Wording may change — never parse it. |
detail | array | opt | Per-field validation detail. Each item carries reason (why it failed); field-specific items also carry field (dotted path, e.g. business.company_number). Omitted on errors that are not field-level. |
correlation_id | string | always | cor_-prefixed saga ID. Also returned in the X-CC-Correlation-Id response header. Quote it to support. |
retryable | boolean | always | Machine signal: true means a retry (same body, same Idempotency-Key) may succeed. Do not infer this from HTTP status alone. |
retry_after_ms | integer | opt | Advised wait in milliseconds before retrying. Mirrors the Retry-After response header (seconds). Present only when retryable is true and a specific wait is known. |
Two tracing headers appear on every response: X-Request-Id (per-hop ID, present on successes too) and X-CC-Correlation-Id (the saga correlation_id, present on errors). Log both so a single line in your logs maps to a single line in ours. When contacting support, quote correlation_id — not X-Request-Id.
Error categories
Use the HTTP status for coarse routing, then switch on code for precise handling. The retryable field is the authoritative machine signal — never infer it from the status alone.
HTTP status map
The full set of statuses the API returns and how your client should treat each.
| Status | When | Retry? |
|---|---|---|
| 200 OK | Request succeeded. | — |
| 201 Created | A resource was created (e.g. an application). | — |
| 202 Accepted | Accepted for async processing (e.g. a disbursement queued). | — |
| 401 Unauthorized | No valid credential. | No — refresh the token. |
| 403 Forbidden | Valid credential, insufficient scope. | No — request the scope. |
| 404 Not Found | Unknown ID, or hidden by your permissions. | No. |
| 409 Conflict | State conflict, or an Idempotency-Key reused with a different body. | No — reconcile first. |
| 413 Payload Too Large | Request body exceeds the platform size limit. Shrink the payload and retry. | No — shrink the payload. |
| 422 Unprocessable | Validation failed — malformed JSON, a missing required field, or a value out of range. | No — fix the request. |
| 423 Locked | The resource is locked and cannot be mutated in its current state (e.g. a declined decision). | No — check the resource state. |
| 429 Too Many Requests | Rate limit exceeded. Honour Retry-After. | Yes — with back-off. |
| 500 Server Error | An unexpected fault on our side. | Yes — idempotent only. |
| 503 Unavailable | Temporary maintenance or overload. | Yes — back-off; check status. |
Common error codes
Switch on code for predictable handling. These are stable identifiers — new codes may be added, so treat an unknown code as a generic failure of its HTTP status class.
| code | HTTP | What it means |
|---|---|---|
validation_failed | 422 | Request failed validation; detail[] carries per-field specifics (field path + reason). |
unauthenticated | 401 | No valid credential, or credential is revoked or unrecognised. |
token_expired | 401 | Bearer token has passed its expiry — refresh it. |
insufficient_scope | 403 | Key not granted the scope this endpoint requires. |
capability_denied | 403 | A staff capability or feature flag is required; check your permissions. |
not_found | 404 | No object exists for the supplied ID, or it is hidden by your permissions. |
conflict | 409 | State conflict, or an Idempotency-Key reused with a different body. |
payload_too_large | 413 | Request body exceeds the platform limit. Reduce payload size — never retryable unchanged. |
decision_locked | 423 | Decision is locked; only an owner-sudo release can move it. |
rate_limited | 429 | Request rate over the quota; honour retry_after_ms and Retry-After. |
internal_error | 500 | Unexpected fault. Retry idempotent calls; then contact support with the correlation_id. |
service_unavailable | 503 | Temporary unavailability. Retry with back-off; check status. |
A validation error in full
Posting to the MCP endpoint without the required method field returns 422 with validation_failed and a per-field detail array:
curl -i "$CC_BASE/partner/v1/mcp" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1 }' HTTP/2 422 X-CC-Correlation-Id: cor_01J2K3M4N5P6Q7R8S9T0A1V2W3 X-Request-Id: req_01J2K3M4N5P6Q7R8S9T0X1V2W3 Content-Type: application/json { "error": { "code": "validation_failed", "message": "The request failed validation.", "detail": [{ "field": "method", "reason": "required" }], "correlation_id": "cor_01J2K3M4N5P6Q7R8S9T0A1V2W3", "retryable": false } }
Handling errors in the SDKs
The SDKs raise typed exceptions for each error category, each exposing code, detail and correlationId. Catch the specific exception you can recover from and let the rest bubble.
use Credicorp\Exception\{InvalidRequestException, RateLimitException, ApiErrorException}; try { $result = $cc->mcp->call('tools/call', $params); } catch (InvalidRequestException $e) { // $e->getCode() === 'validation_failed', $e->getDetail() has per-field info log_warning($e->getMessage(), $e->getCorrelationId()); } catch (RateLimitException $e) { sleep($e->getRetryAfter()); // then retry } catch (ApiErrorException $e) { // transient — safe to retry idempotent calls with back-off }
try { const result = await cc.mcp.call('tools/call', params); } catch (err) { if (err.retryable && err.retryAfterMs) { await sleep(err.retryAfterMs); // then retry } else if (!err.retryable) { console.warn(err.code, err.detail, err.correlationId); } else { throw err; } }
import credicorp try: result = cc.mcp.call('tools/call', **params) except credicorp.error.InvalidRequestError as e: log.warning("%s detail=%s (%s)", e.code, e.detail, e.correlation_id) except credicorp.error.RateLimitError as e: time.sleep(e.retry_after_ms / 1000) # then retry except credicorp.error.APIError: pass # transient — retry idempotent calls with back-off
Retry only idempotent requests on 5xx and 429, and always send the same Idempotency-Key so a retried POST can't double-create. See the idempotency guide.
