Skip to main content

Error Handling

Best practices for handling errors and edge cases in your integration.

This guide covers error handling strategies for Paysera Checkout integrations.

Error Response Format​

All API errors return a consistent JSON structure:

{
"error": "error_code",
"error_description": "Human-readable description"
}

Field-validation errors additionally include error_properties — a map of field path → list of message strings:

{
"error": "invalid_properties",
"error_description": "Validation failed",
"error_properties": {
"purchase.amount": ["Amount must be greater than 0"],
"purchase.currency": ["Currency is required"]
}
}

Match on the machine-readable error code, not on error_description (which may change or be localized).

HTTP Status Codes​

CodeMeaningRetry
200SuccessN/A
201CreatedN/A
400Bad Request / field validation (invalid_properties)No
401UnauthorizedMaybe*
403ForbiddenNo
404Not FoundNo
409ConflictNo
422Business-rule validationNo
429Rate LimitedYes (with backoff)
500Server ErrorYes (with backoff)
502Bad GatewayYes (with backoff)
503Service UnavailableYes (with backoff)

*Retry after refreshing access token

Common Errors​

Authentication Errors​

Invalid Credentials (401)​

{
"error": "invalid_client",
"error_description": "Invalid client credentials"
}

Solution: Verify client_id and client_secret are correct.

Expired Token (401)​

{
"error": "invalid_token",
"error_description": "Token is expired"
}

Solution: Request a new access token.

Invalid Token (401)​

{
"error": "invalid_token",
"error_description": "Token validation failed"
}

Solution: Check token format and ensure you're using the correct environment.

Field Validation (400)​

{
"error": "invalid_properties",
"error_description": "Validation failed",
"error_properties": {
"purchase.amount": ["Amount must be greater than 0"],
"purchase.currency": ["Currency is required"]
}
}

Solution: Fix the fields listed in error_properties and retry. Each key is a field path; each value is a list of messages for that field.

Business-rule Validation (422)​

Some requests are well-formed but violate a business rule — for example a split request that conflicts with the project's policy. These return 422 with a specific error code (and sometimes error_properties):

{
"error": "variadic_entry_required",
"error_description": "The configured split policy requires exactly one variadic entry."
}

Solution: Handle by error code. See Error Codes for the full catalog (including all split-payment codes).

Not Found (404)​

{
"error": "not_found",
"error_description": "Order not found"
}

Solution: Verify the resource ID is correct.

Conflict (409)​

{
"error": "conflict",
"error_description": "Order already completed"
}

Solution: The resource is in a state that doesn't allow the operation (for example, modifying an already-completed order). Note that order reference is not required to be unique — a duplicate reference does not cause a conflict.

Rate Limited (429)​

{
"error": "rate_limited",
"error_description": "Too many requests"
}

Response Headers:

Retry-After: 60
X-RateLimit-Limit: 50
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705320000

Solution: Implement exponential backoff and respect Retry-After header.

Error Handling Code Examples​

Each sample reads error_description for the human-readable message and, on a 400, iterates the error_properties map (field → list of messages).

<?php

class PayseraClient
{
private string $baseUrl;
private ?string $accessToken = null;

public function request(string $method, string $endpoint, array $data = []): array
{
$maxRetries = 3;
$attempt = 0;

while ($attempt < $maxRetries) {
try {
return $this->doRequest($method, $endpoint, $data);
} catch (RateLimitException $e) {
$retryAfter = $e->getRetryAfter();
sleep($retryAfter);
$attempt++;
} catch (TokenExpiredException $e) {
$this->refreshToken();
$attempt++;
} catch (ServerException $e) {
// Exponential backoff
sleep(pow(2, $attempt));
$attempt++;
}
}

throw new MaxRetriesExceededException();
}

private function doRequest(string $method, string $endpoint, array $data): array
{
$ch = curl_init($this->baseUrl . $endpoint);

curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $this->accessToken,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $method !== 'GET' ? json_encode($data) : null,
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$body = json_decode($response, true);
$description = $body['error_description'] ?? ($body['error'] ?? 'Unknown error');

return match (true) {
$httpCode >= 200 && $httpCode < 300 => $body,
$httpCode === 401 => throw new TokenExpiredException($description),
$httpCode === 400 => throw new ValidationException($description, $body['error_properties'] ?? []),
$httpCode === 429 => throw new RateLimitException($description),
$httpCode >= 500 => throw new ServerException($description),
default => throw new ApiException($description, $httpCode),
};
}
}

// Usage with error handling
try {
$order = $client->request('POST', '/merchant-order/integration/v1/orders', [
'purchase' => ['reference' => 'ORDER-123', 'amount' => 2500, 'currency' => 'EUR'],
]);
} catch (ValidationException $e) {
foreach ($e->getProperties() as $field => $messages) {
foreach ($messages as $message) {
echo "Error in {$field}: {$message}\n";
}
}
} catch (ApiException $e) {
error_log("API error: " . $e->getMessage());
}

Troubleshooting​

Common Issues​

SymptomPossible CauseSolution
401 on all requestsWrong credentialsVerify client_id/secret
401 after workingToken expiredImplement token refresh
400 invalid_properties on valid dataWrong field formatCheck the field paths in error_properties
422 on a well-formed requestBusiness rule violated (e.g. split policy)Handle by error code — see Error Codes
429 frequentlyToo many requestsImplement rate limiting
500 errorsServer issueRetry with backoff

Debug Checklist​

  1. Check credentials - Correct client_id and client_secret?
  2. Check URL - Using correct API base URL?
  3. Check token - Is token expired? Is it included in header?
  4. Check request body - Is JSON valid? Are all required fields present?
  5. Check amount format - Is it an integer in minor units (1000 for €10.00) not a decimal (10.00)?
  6. Check rate limits - Are you under the limit?