Error Handling
The XCO API uses standard HTTP status codes and a consistent error format based on RFC 7807 (Problem Details). This guide explains how to interpret and handle errors across all Express Checkout endpoints.
Error Response Format
All error responses follow the application/problem+json content type with this structure:
{
"type": "https://api.purse.eu/problems/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "The request body contains invalid fields.",
"instance": "/v1/merchants/123e4567-e89b-12d3-a456-426614174000/users",
"errors": [
{
"detail": "must be a valid email address",
"pointer": "/user/email"
}
]
}
| Field | Type | Description |
|---|---|---|
type | string | A URI reference identifying the problem type |
title | string | A short, human-readable summary of the problem |
status | integer | The HTTP status code (mirrored in the body for convenience) |
detail | string | A human-readable explanation specific to this occurrence |
instance | string | The API path that generated the error |
error | string | Single error detail (when applicable) |
errors | array | List of field-level validation errors (when applicable) |
When present, each entry in the errors array contains:
detail— A description of what's wrong (e.g.,"must be a valid email address")pointer— A JSON Pointer to the offending field (e.g.,/user/email)
HTTP Status Codes
Overview
| Status Code | Meaning | Retryable? |
|---|---|---|
400 | Bad Request — Invalid input, missing required fields, or malformed data | ❌ Fix the request |
401 | Unauthorized — Missing, expired, or invalid access token | ⚠️ Refresh token, then retry |
403 | Forbidden — Valid token but insufficient permissions or unsupported origin | ❌ Check configuration |
404 | Not Found — Merchant, user, or consent challenge not found | ❌ Verify identifiers |
429 | Too Many Requests — Rate limit exceeded | ✅ Retry with backoff |
500 | Internal Server Error — Unexpected server-side failure | ✅ Retry with backoff |
Status Codes per Endpoint
| Endpoint | 400 | 401 | 403 | 404 | 429 | 500 |
|---|---|---|---|---|---|---|
GET /v1/users/exists | ✅ | ✅ | — | — | ✅ | — |
POST /v1/merchants/{merchantId}/users | ✅ | ✅ | ✅ | ✅ | — | — |
POST .../passwordless/start | ✅ | ✅ | — | ✅ | ✅ | ✅ |
POST .../passwordless/verify | ✅ | ✅ | — | ✅ | ✅ | ✅ |
POST .../consents | ✅ | ✅ | — | ✅ | ✅ | — |
GET .../users/{customer_reference} | ✅ | ✅ | ✅ | ✅ | — | — |
Common Error Scenarios
1. Validation Errors (400)
Validation errors occur when the request payload is malformed or missing required fields.
- Missing Parameters
- Invalid Field Format
- Invalid OTP Code
Scenario: Calling GET /v1/users/exists without email or phone_number.
{
"type": "https://api.purse.eu/problems/validation-error",
"title": "Bad Request",
"status": 400,
"detail": "At least one of email or phone_number must be provided",
"instance": "/v1/users/exists"
}
Resolution: Provide at least one of email or phone_number as a query parameter.
Scenario: Providing an invalid email format in the user creation payload.
{
"type": "https://api.purse.eu/problems/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "The request contains invalid fields",
"instance": "/v1/users/exists",
"errors": [
{
"detail": "must be a valid email address",
"pointer": "/email"
}
]
}
Resolution: Check the errors array — the pointer field tells you exactly which field is invalid.
Scenario: The customer enters an incorrect verification code during passwordless verification.
{
"type": "https://api.purse.eu/problems/validation-error",
"title": "Bad Request",
"status": 400,
"detail": "Invalid verification code",
"instance": "/v1/merchants/{merchantId}/users/passwordless/verify"
}
Resolution: Prompt the customer to re-enter the OTP code. If repeated failures occur, offer to resend a new code via the passwordless/start endpoint.
2. Authentication Errors (401)
Never expose your CLIENT_SECRET or ACCESS_TOKEN in frontend code or client-side logs.
A 401 error means the request is not authenticated. Common causes:
| Cause | Resolution |
|---|---|
Missing Authorization header | Add Authorization: Bearer ${ACCESS_TOKEN} to the request |
| Expired access token | Request a new token via the OAuth2 flow |
| Invalid token | Verify the token was generated with the xco-api scope |
Example handling:
async function callXcoApi(url, options) {
let response = await fetch(url, options);
if (response.status === 401) {
// Token expired — refresh and retry once
const newToken = await refreshAccessToken();
options.headers['Authorization'] = `Bearer ${newToken}`;
response = await fetch(url, options);
}
return response;
}
3. Forbidden Errors (403)
A 403 error indicates that the token is valid but the request is not allowed. This typically happens when:
- The merchant origin is not whitelisted for the XCO API.
- The OAuth2 token does not include the
xco-apiscope.
Contact the Purse Integration Team to ensure your domain and merchant account are properly configured for Express Checkout access.
4. Not Found Errors (404)
A 404 error means the referenced resource does not exist.
| Endpoint | Possible Cause |
|---|---|
POST /v1/merchants/{merchantId}/users | The merchantId is invalid or does not exist |
POST .../passwordless/start | The merchantId does not exist |
POST .../passwordless/verify | The merchantId does not exist |
POST .../consents | The merchantId or consent_challenge_id is invalid |
GET .../users/{customer_reference} | The customer_reference does not exist for this merchant, or no active consent exists |
The consent_challenge_id returned by the passwordless verify step is short-lived. If you receive a 404 when creating a consent, the challenge may have expired. Restart the passwordless flow from the start step.
5. Rate Limiting (429)
A 429 response means you have exceeded the allowed number of requests. See the Rate Limiting guide for full details.
Key points:
- Always pass a
caller_idparameter (e.g., the customer's browser IP) to enable granular rate limiting per end-user rather than perclientId. - Without
caller_id, all requests from your backend share the same rate limit bucket, which may block legitimate customers.
Example response:
{
"type": "https://api.purse.eu/problems/rate-limit-exceeded",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded. Please retry after a short delay.",
"instance": "/v1/merchants/{merchantId}/users/passwordless/start"
}
6. Server Errors (500)
A 500 error indicates an internal failure on the XCO platform side. This can occur on the passwordless endpoints when the authentication service is temporarily unavailable.
Resolution: Implement retry logic with exponential backoff (see Best Practice Summary below). If the issue persists, contact Purse support.
Error Handling by Flow
User Existence Check
User Creation
Passwordless Flow
Best Practice Summary
| Practice | Description |
|---|---|
Always send caller_id | Prevents rate limit issues by enabling per-user granularity |
Parse the errors array | For 400 responses, iterate over errors to display field-specific messages to the user |
| Implement token refresh | Automatically refresh the OAuth2 token on 401 before retrying |
| Use exponential backoff | For 429 and 500 errors, backoff with jitter to avoid thundering herd |
| Handle challenge expiration | If consent creation returns 404, restart the passwordless flow |
| Log error details | Log the full ProblemDetail response for debugging — but never log tokens or secrets |
| Show user-friendly messages | Translate API errors into actionable messages for the end user |
Next Steps
- Rate Limiting — Detailed rate limit thresholds and
caller_idusage - Getting Started — Step-by-step integration guide
- XCO API Reference — Full OpenAPI specification