# Error Codes and Error Handling

When an API request fails, the Peach API returns a structured error response with a consistent format across all endpoints. This page documents the error response structure, common error scenarios, and best practices for handling errors in your integration.

## Error response format

Every error response from the Peach API follows the same JSON structure:


```json
{
  "status": 400,
  "message": "A human-readable description of the error.",
  "detail": "A human-readable description of the error.",
  "errorType": "BadRequest",
  "data": null
}
```

| Field | Type | Description |
|  --- | --- | --- |
| `status` | integer | The HTTP status code of the response. |
| `message` | string | A human-readable description of the error. Use this field for logging and display. |
| `detail` | string or null | Deprecated. Contains the same value as `message`. Use `message` instead. |
| `errorType` | string | A machine-readable error code that identifies the specific type of error. Use this field for programmatic error handling. |
| `data` | object or null | Additional structured data about the error, when available. For validation errors, this typically contains a map of field names to error descriptions. |


Some error types include additional fields:

**Internal server errors** include an `errorId` field containing a UUID. Provide this identifier when contacting Peach support to help locate the relevant logs.


```json
{
  "status": 500,
  "message": "Internal Server Error",
  "detail": "Internal Server Error",
  "errorType": "InternalServerError",
  "errorId": "550e8400-e29b-41d4-a716-446655440000",
  "data": null
}
```

**Validation errors** often include a `data` object with per-field error details:


```json
{
  "status": 400,
  "message": "Invalid parameters",
  "detail": "Invalid parameters",
  "errorType": "BadRequest",
  "data": {
    "errors": {
      "dateOfBirth": "Must be a valid date in YYYY-MM-DD format.",
      "email": "Not a valid email address."
    }
  }
}
```

## Common error scenarios and troubleshooting

This section covers error scenarios that integration engineers frequently encounter, along with their causes and resolutions.

### 1. Duplicate external ID on resource creation

**Error response:**


```json
{
  "status": 409,
  "message": "External IDs must be unique - 'borrower-12345' already exists.",
  "errorType": "Duplicate"
}
```

**Cause:** You are creating a resource (borrower, loan, payment instrument, etc.) with an `externalId` that is already associated with another resource of the same type in your company.

**Resolution:** Use a unique `externalId` for each resource. If you are retrying a failed creation request, first check whether the previous request actually succeeded by fetching the resource by its `externalId`. If it exists, use the existing resource instead of creating a new one.

### 2. Authentication failure

**Error response:**


```json
{
  "status": 401,
  "message": "No authorization token provided",
  "errorType": "OAuthProblem"
}
```

**Cause:** The request is missing the `X-API-Key` header, or the provided API key or bearer token is invalid, expired, or revoked.

**Resolution:** Verify that your request includes the `X-API-Key` header with a valid key. If using bearer tokens, check that the token has not expired and refresh it if necessary. See [API Keys](/developer-tools/api-keys) for details on managing keys.

### 3. Insufficient permissions

**Error response:**


```json
{
  "status": 403,
  "message": "User does not have the required permissions.",
  "errorType": "Forbidden"
}
```

**Cause:** The API key's role does not include the permission required for the endpoint and HTTP method you are calling.

**Resolution:** Check which permissions are assigned to your API key's role. Work with your Peach administrator to add the necessary permission. Peach permissions follow the format `resource:action` (for example, `loan:create` or `transaction:read`).

### 4. Resource not found

**Error response:**


```json
{
  "status": 404,
  "message": "Person not found.",
  "errorType": "NotFound"
}
```

**Cause:** The resource identifier in the URL does not match any existing resource. This can happen if the ID is incorrect, the resource was deleted, or you do not have permission to access the resource.

**Resolution:** Verify the resource identifier is correct. Confirm that you are using the right API key (associated with the correct company). If using external IDs, ensure you are passing them with the correct prefix (e.g., `ext-` prefix for external identifiers).

### 5. Invalid URL or endpoint path

**Error response:**


```json
{
  "status": 404,
  "message": "The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.",
  "errorType": "UrlNotDefined"
}
```

**Cause:** The URL path does not match any endpoint defined in the API. This is different from a `NotFound` error, which means the endpoint exists but the resource does not.

**Resolution:** Double-check the endpoint path against the [API reference](/api-docs/api-public/). Verify that you are using the correct base URL for your environment (`https://sandboxapi.peach.finance` for sandbox, `https://api.peach.finance` for production). Ensure the path segments are in the correct order and properly URL-encoded.

### 6. Loan locked during concurrent operations

**Error response:**


```json
{
  "status": 423,
  "message": "Loan is currently locked for processing.",
  "errorType": "LoanLocked"
}
```

**Cause:** A background process (such as payment application, interest accrual, or a loan replay) is currently modifying the loan. The API returns this error to prevent conflicting concurrent writes.

**Resolution:** Retry the request after a short delay (starting with 1-2 seconds). Use exponential backoff if the lock persists. If you are triggering multiple operations on the same loan in sequence, wait for each operation to complete (by polling the loan status or listening for [webhook events](/getting-started/webhooks/about-webhooks)) before initiating the next one.

### 7. Configuration mismatch on loan operation

**Error response:**


```json
{
  "status": 409,
  "message": "The requested operation is not compatible with the current configuration.",
  "errorType": "ConfigMismatch"
}
```

**Cause:** The operation you are attempting is not supported by the loan's current type configuration. Examples include trying to create a draw on an installment loan (draws are only valid for lines of credit), applying a fee type that is not configured in the loan type, or scheduling a payment using a method not enabled for the product.

**Resolution:** Review the loan type configuration to confirm the operation is supported. If you need to support the operation, update the loan type configuration before retrying. Check the `message` field for specifics about which configuration is incompatible.

### 8. Duplicate key error on external ID

**Error response:**


```json
{
  "status": 409,
  "message": "External IDs must be unique - 'PMT-98765' already exists.",
  "errorType": "Duplicate"
}
```

**Cause:** A database uniqueness constraint was violated. This most commonly occurs when creating a resource with an `externalId` that already exists within the same company.

**Resolution:** This error is safe to treat as an idempotency signal. If you are retrying a creation request, fetch the existing resource by its `externalId` to confirm it was created successfully on a previous attempt. If you intended to create a new, distinct resource, use a different `externalId`.

### 9. Communication template errors

**Error response:**


```json
{
  "status": 409,
  "message": "No borrower contact matching the given criteria was found.",
  "errorType": "NoSuitableContact"
}
```

**Cause:** You attempted to send a communication (email, SMS, etc.) to a borrower, but the borrower has no contact entry of the required type, or the matching contact is marked as invalid or unsubscribed.

**Resolution:** Verify that the borrower has a valid contact of the required type (email address for email communications, phone number for SMS). Add or update the borrower's contact information before retrying the communication.

### 10. Invalid public ID format

**Error response:**


```json
{
  "status": 422,
  "message": "Invalid public ID 'abc123'",
  "errorType": "InvalidPublicId"
}
```

**Cause:** The resource identifier provided in the URL path does not match the expected format for Peach public IDs. Peach IDs follow a specific format with a resource-type prefix.

**Resolution:** Verify you are using the correct identifier. Peach public IDs have a specific prefix per resource type (e.g., `BO-` for borrowers, `LN-` for loans). If you are passing an external ID, use the appropriate external ID lookup endpoint or prefix.

### 11. Rate limiting on login attempts

**Error response:**


```json
{
  "status": 429,
  "message": "This account has been locked due to too many failed attempts. Please try again later."
}
```

**Cause:** Too many failed authentication attempts were made for the same account within a short time window. The account is temporarily locked as a security measure.

**Resolution:** Wait for the lockout period to expire before retrying. Do not continue to retry login attempts during the lockout, as this will extend the lockout duration. Review your authentication flow to ensure credentials are being sent correctly.

## Error handling best practices

### Use errorType for programmatic handling

Always use the `errorType` field rather than the `message` field for programmatic error handling. The `errorType` value is a stable, machine-readable identifier, while `message` is a human-readable string that may change between API versions.


```python
response = requests.post(url, json=payload, headers=headers)

if response.status_code != 200:
    error = response.json()
    error_type = error.get("errorType")

    if error_type == "Duplicate":
        # Handle duplicate resource - fetch existing instead
        existing = fetch_by_external_id(external_id)
    elif error_type == "LoanLocked":
        # Retry after delay
        time.sleep(2)
        retry_request()
    elif error_type == "Forbidden":
        # Log permission issue for investigation
        log.error(f"Permission denied: {error.get('message')}")
    else:
        # Log unexpected error
        log.error(f"API error: {error_type} - {error.get('message')}")
```

### Implement retry logic with backoff

Certain errors are transient and should be retried with exponential backoff:

- **423 LoanLocked**: The loan is being processed. Retry after 1-2 seconds with exponential backoff up to 30 seconds.
- **412 ReadReplicaNotReady**: The read replica is catching up. Retry after 500ms-1 second.
- **429 Too Many Requests**: Rate limited. Wait for the lockout period to expire.
- **500 InternalServerError**: A server-side issue. Retry with backoff, but stop after 3-5 attempts. If the error persists, contact support with the `errorId`.
- **504 Timeout**: The request timed out. Retry with increasing delays.


Do not retry **400**, **401**, **403**, **404**, or **409** errors. These indicate client-side issues that require changes to the request before retrying.

### Use external IDs for idempotency

When creating resources (borrowers, loans, payment instruments, draws, payments), always provide an `externalId` that is unique and deterministic for the operation. If a creation request fails due to a network error and you are unsure whether it succeeded, you can safely retry with the same `externalId`. If the resource was already created, the API returns a `409 Duplicate` error, which you can handle by fetching the existing resource.

### Capture errorId for server errors

When you receive a `500 InternalServerError`, always log the `errorId` from the response. This UUID is the fastest way for Peach support to locate the relevant server logs when investigating an issue.

### Subscribe to webhooks for async operations

Many Peach operations are processed asynchronously (payment application, interest accrual, statement generation). Rather than polling the API, subscribe to [webhook events](/getting-started/webhooks/about-webhooks) to be notified when these operations complete or fail. This reduces the chance of hitting rate limits and provides faster feedback.

### Validate requests client-side

The Peach API publishes an OpenAPI specification. Use it to validate request bodies in your application before sending them to the API. This catches formatting errors, missing required fields, and invalid enum values before they reach the server, reducing 400-level errors and improving your integration's reliability.