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",
"message": "Human-readable description",
"details": [
{
"field": "field_name",
"message": "Field-specific error message"
}
]
}
HTTP Status Codes​
| Code | Meaning | Retry |
|---|---|---|
| 200 | Success | N/A |
| 201 | Created | N/A |
| 400 | Bad Request | No |
| 401 | Unauthorized | Maybe* |
| 403 | Forbidden | No |
| 404 | Not Found | No |
| 409 | Conflict | No |
| 422 | Validation Error | No |
| 429 | Rate Limited | Yes (with backoff) |
| 500 | Server Error | Yes (with backoff) |
| 502 | Bad Gateway | Yes (with backoff) |
| 503 | Service Unavailable | Yes (with backoff) |
*Retry after refreshing access token
Common Errors​
Authentication Errors​
Invalid Credentials (401)​
{
"error": "invalid_client",
"message": "Invalid client credentials"
}
Solution: Verify client_id and client_secret are correct.
Expired Token (401)​
{
"error": "invalid_token",
"message": "Token is expired"
}
Solution: Request a new access token.
Invalid Token (401)​
{
"error": "invalid_token",
"message": "Token validation failed"
}
Solution: Check token format and ensure you're using the correct environment.
Validation Errors (422)​
{
"error": "validation_error",
"message": "Invalid request parameters",
"details": [
{
"field": "purchase.amount.amount",
"message": "Amount must be greater than 0"
},
{
"field": "purchase.amount.currency",
"message": "Currency is required"
}
]
}
Solution: Fix the indicated fields and retry.
Not Found (404)​
{
"error": "not_found",
"message": "Order not found"
}
Solution: Verify the resource ID is correct.
Conflict (409)​
{
"error": "conflict",
"message": "Order with this reference already exists"
}
Solution: Use a unique reference or handle the existing order.
Rate Limited (429)​
{
"error": "rate_limited",
"message": "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​
- PHP
- JavaScript
- Python
- Kotlin
- Go
- C#
<?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);
return match (true) {
$httpCode >= 200 && $httpCode < 300 => $body,
$httpCode === 401 => throw new TokenExpiredException($body['message']),
$httpCode === 422 => throw new ValidationException($body['message'], $body['details']),
$httpCode === 429 => throw new RateLimitException($body['message']),
$httpCode >= 500 => throw new ServerException($body['message']),
default => throw new ApiException($body['message'], $httpCode),
};
}
}
// Usage with error handling
try {
$order = $client->request('POST', '/merchant-order/integration/v1/orders', [
'project_id' => $projectId,
'purchase' => ['reference' => 'ORDER-123', 'amount' => '2500', 'currency' => 'EUR'],
]);
} catch (ValidationException $e) {
foreach ($e->getDetails() as $detail) {
echo "Error in {$detail['field']}: {$detail['message']}\n";
}
} catch (ApiException $e) {
error_log("API error: " . $e->getMessage());
}
class PayseraClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.accessToken = null;
}
async request(method, endpoint, data = null) {
const maxRetries = 3;
let attempt = 0;
while (attempt < maxRetries) {
try {
return await this.doRequest(method, endpoint, data);
} catch (error) {
if (error instanceof RateLimitError) {
await this.sleep(error.retryAfter * 1000);
attempt++;
} else if (error instanceof TokenExpiredError) {
await this.refreshToken();
attempt++;
} else if (error instanceof ServerError) {
await this.sleep(Math.pow(2, attempt) * 1000);
attempt++;
} else {
throw error;
}
}
}
throw new MaxRetriesError();
}
async doRequest(method, endpoint, data) {
const response = await fetch(this.baseUrl + endpoint, {
method,
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
},
body: data ? JSON.stringify(data) : undefined,
});
const body = await response.json();
if (response.ok) {
return body;
}
switch (response.status) {
case 401:
throw new TokenExpiredError(body.message);
case 422:
throw new ValidationError(body.message, body.details);
case 429:
throw new RateLimitError(body.message, response.headers.get('Retry-After'));
default:
if (response.status >= 500) {
throw new ServerError(body.message);
}
throw new ApiError(body.message, response.status);
}
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage
try {
const order = await client.request('POST', '/merchant-order/integration/v1/orders', {
project_id: projectId,
purchase: { reference: 'ORDER-123', amount: '2500', currency: 'EUR' },
});
} catch (error) {
if (error instanceof ValidationError) {
error.details.forEach(detail => {
console.error(`Error in ${detail.field}: ${detail.message}`);
});
} else {
console.error('API error:', error.message);
}
}
import time
import requests
from typing import Optional, Dict, Any
class PayseraClient:
def __init__(self, base_url: str):
self.base_url = base_url
self.access_token: Optional[str] = None
def request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Dict[str, Any]:
max_retries = 3
attempt = 0
while attempt < max_retries:
try:
return self._do_request(method, endpoint, data)
except RateLimitError as e:
time.sleep(e.retry_after)
attempt += 1
except TokenExpiredError:
self._refresh_token()
attempt += 1
except ServerError:
time.sleep(2 ** attempt)
attempt += 1
raise MaxRetriesError()
def _do_request(self, method: str, endpoint: str, data: Optional[Dict]) -> Dict[str, Any]:
response = requests.request(
method,
self.base_url + endpoint,
headers={
'Authorization': f'Bearer {self.access_token}',
'Content-Type': 'application/json',
},
json=data,
)
body = response.json()
if response.ok:
return body
if response.status_code == 401:
raise TokenExpiredError(body['message'])
elif response.status_code == 422:
raise ValidationError(body['message'], body.get('details', []))
elif response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
raise RateLimitError(body['message'], retry_after)
elif response.status_code >= 500:
raise ServerError(body['message'])
else:
raise ApiError(body['message'], response.status_code)
# Usage
try:
order = client.request('POST', '/merchant-order/integration/v1/orders', {
'project_id': project_id,
'purchase': {'reference': 'ORDER-123', 'amount': '2500', 'currency': 'EUR'},
})
except ValidationError as e:
for detail in e.details:
print(f"Error in {detail['field']}: {detail['message']}")
except ApiError as e:
print(f"API error: {e.message}")
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import kotlinx.serialization.json.*
import kotlin.math.pow
class PayseraClient(private val baseUrl: String) {
private var accessToken: String? = null
private val client = HttpClient.newHttpClient()
suspend fun request(method: String, endpoint: String, data: JsonObject? = null): JsonObject {
val maxRetries = 3
var attempt = 0
while (attempt < maxRetries) {
try {
return doRequest(method, endpoint, data)
} catch (e: RateLimitException) {
Thread.sleep(e.retryAfter * 1000L)
attempt++
} catch (e: TokenExpiredException) {
refreshToken()
attempt++
} catch (e: ServerException) {
Thread.sleep((2.0.pow(attempt) * 1000).toLong())
attempt++
}
}
throw MaxRetriesException()
}
private fun doRequest(method: String, endpoint: String, data: JsonObject?): JsonObject {
val requestBuilder = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + endpoint))
.header("Authorization", "Bearer $accessToken")
.header("Content-Type", "application/json")
when (method) {
"GET" -> requestBuilder.GET()
"POST" -> requestBuilder.POST(HttpRequest.BodyPublishers.ofString(data?.toString() ?: ""))
}
val response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
val body = Json.parseToJsonElement(response.body()).jsonObject
return when {
response.statusCode() in 200..299 -> body
response.statusCode() == 401 -> throw TokenExpiredException(body["message"]?.jsonPrimitive?.content ?: "")
response.statusCode() == 422 -> throw ValidationException(body["message"]?.jsonPrimitive?.content ?: "", body["details"]?.jsonArray ?: buildJsonArray {})
response.statusCode() == 429 -> throw RateLimitException(body["message"]?.jsonPrimitive?.content ?: "")
response.statusCode() >= 500 -> throw ServerException(body["message"]?.jsonPrimitive?.content ?: "")
else -> throw ApiException(body["message"]?.jsonPrimitive?.content ?: "", response.statusCode())
}
}
}
// Usage
try {
val order = client.request("POST", "/merchant-order/integration/v1/orders", buildJsonObject {
put("project_id", projectId)
putJsonObject("purchase") {
put("reference", "ORDER-123")
put("amount", "2500")
put("currency", "EUR")
}
})
} catch (e: ValidationException) {
e.details.forEach { detail ->
println("Error in ${detail.jsonObject["field"]}: ${detail.jsonObject["message"]}")
}
} catch (e: ApiException) {
println("API error: ${e.message}")
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"strconv"
"time"
)
type PayseraClient struct {
BaseURL string
AccessToken string
}
func (c *PayseraClient) Request(method, endpoint string, data interface{}) (map[string]interface{}, error) {
maxRetries := 3
attempt := 0
for attempt < maxRetries {
result, err := c.doRequest(method, endpoint, data)
if err != nil {
switch e := err.(type) {
case *RateLimitError:
time.Sleep(time.Duration(e.RetryAfter) * time.Second)
attempt++
case *TokenExpiredError:
c.refreshToken()
attempt++
case *ServerError:
time.Sleep(time.Duration(math.Pow(2, float64(attempt))) * time.Second)
attempt++
default:
return nil, err
}
} else {
return result, nil
}
}
return nil, fmt.Errorf("max retries exceeded")
}
func (c *PayseraClient) doRequest(method, endpoint string, data interface{}) (map[string]interface{}, error) {
var body io.Reader
if data != nil {
jsonData, _ := json.Marshal(data)
body = bytes.NewBuffer(jsonData)
}
req, _ := http.NewRequest(method, c.BaseURL+endpoint, body)
req.Header.Set("Authorization", "Bearer "+c.AccessToken)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var result map[string]interface{}
json.Unmarshal(respBody, &result)
switch {
case resp.StatusCode >= 200 && resp.StatusCode < 300:
return result, nil
case resp.StatusCode == 401:
return nil, &TokenExpiredError{Message: result["message"].(string)}
case resp.StatusCode == 422:
return nil, &ValidationError{Message: result["message"].(string), Details: result["details"]}
case resp.StatusCode == 429:
retryAfter, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
return nil, &RateLimitError{Message: result["message"].(string), RetryAfter: retryAfter}
case resp.StatusCode >= 500:
return nil, &ServerError{Message: result["message"].(string)}
default:
return nil, &ApiError{Message: result["message"].(string), StatusCode: resp.StatusCode}
}
}
// Usage
func main() {
order, err := client.Request("POST", "/merchant-order/integration/v1/orders", map[string]interface{}{
"project_id": projectId,
"purchase": map[string]interface{}{
"reference": "ORDER-123",
"amount": "2500",
"currency": "EUR",
},
})
if err != nil {
switch e := err.(type) {
case *ValidationError:
for _, detail := range e.Details.([]interface{}) {
d := detail.(map[string]interface{})
fmt.Printf("Error in %s: %s\n", d["field"], d["message"])
}
default:
fmt.Printf("API error: %s\n", err.Error())
}
}
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
public class PayseraClient
{
private readonly string _baseUrl;
private readonly HttpClient _httpClient;
private string? _accessToken;
public PayseraClient(string baseUrl)
{
_baseUrl = baseUrl;
_httpClient = new HttpClient();
}
public async Task<JsonDocument> RequestAsync(string method, string endpoint, object? data = null)
{
const int maxRetries = 3;
var attempt = 0;
while (attempt < maxRetries)
{
try
{
return await DoRequestAsync(method, endpoint, data);
}
catch (RateLimitException e)
{
await Task.Delay(e.RetryAfter * 1000);
attempt++;
}
catch (TokenExpiredException)
{
await RefreshTokenAsync();
attempt++;
}
catch (ServerException)
{
await Task.Delay((int)Math.Pow(2, attempt) * 1000);
attempt++;
}
}
throw new MaxRetriesException();
}
private async Task<JsonDocument> DoRequestAsync(string method, string endpoint, object? data)
{
var request = new HttpRequestMessage(new HttpMethod(method), _baseUrl + endpoint);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
if (data != null)
{
var json = JsonSerializer.Serialize(data);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var doc = JsonDocument.Parse(body);
if (response.IsSuccessStatusCode)
return doc;
var message = doc.RootElement.GetProperty("message").GetString() ?? "";
return response.StatusCode switch
{
System.Net.HttpStatusCode.Unauthorized => throw new TokenExpiredException(message),
System.Net.HttpStatusCode.UnprocessableEntity => throw new ValidationException(message, doc.RootElement.GetProperty("details")),
System.Net.HttpStatusCode.TooManyRequests => throw new RateLimitException(message, int.Parse(response.Headers.GetValues("Retry-After").First())),
>= System.Net.HttpStatusCode.InternalServerError => throw new ServerException(message),
_ => throw new ApiException(message, (int)response.StatusCode)
};
}
}
// Usage
try
{
var order = await client.RequestAsync("POST", "/merchant-order/integration/v1/orders", new
{
project_id = projectId,
purchase = new { reference = "ORDER-123", amount = "2500", currency = "EUR" }
});
}
catch (ValidationException e)
{
foreach (var detail in e.Details.EnumerateArray())
{
Console.WriteLine($"Error in {detail.GetProperty("field")}: {detail.GetProperty("message")}");
}
}
catch (ApiException e)
{
Console.WriteLine($"API error: {e.Message}");
}
Troubleshooting​
Common Issues​
| Symptom | Possible Cause | Solution |
|---|---|---|
| 401 on all requests | Wrong credentials | Verify client_id/secret |
| 401 after working | Token expired | Implement token refresh |
| 422 on valid data | Wrong field format | Check API documentation |
| 429 frequently | Too many requests | Implement rate limiting |
| 500 errors | Server issue | Retry with backoff |
Debug Checklist​
- Check credentials - Correct client_id and client_secret?
- Check URL - Using correct API base URL?
- Check token - Is token expired? Is it included in header?
- Check request body - Is JSON valid? Are all required fields present?
- Check amount format - Is it a string in minor units (
"1000"for €10.00) not a decimal ("10.00")? - Check rate limits - Are you under the limit?