Error Handling
PayConnect uses standard HTTP status codes and returns structured error responses to help you diagnose and handle issues.
Error Response Format
All error responses — validation and domain/business-rule — follow one consistent structure:
{
"statusCode": 404,
"error": "not_found",
"message": "Customer not found",
"correlationId": "b2f6c1e0-8f4a-4c8b-9d2e-1a2b3c4d5e6f"
}
| Field | Type | Description |
|---|---|---|
statusCode | number | HTTP status code |
error | string | Stable machine-readable code — branch on this, not on message. Values include validation_failed, bad_request, unauthorized, forbidden, not_found, conflict, unprocessable_entity, rate_limited, and internal_error |
message | string | Human-readable error description |
correlationId | string | The request's correlation id — quote it when contacting support (also returned in the CorrelationId response header) |
Field-level errors additionally carry fields[] and validationDetail (below).
Field-level Errors (400)
Failures tied to a specific field — request-schema validation and field-level domain rules (an unknown or wrong-typed custom field value, a duplicate externalUserId) — carry error: "validation_failed" plus per-field detail:
{
"statusCode": 400,
"error": "validation_failed",
"message": "Request validation failed: body.filters.email.operator: Invalid enum value. Expected 'eq' | 'contains' | 'startsWith', received 'like'",
"validationDetail": "body.filters.email.operator: Invalid enum value. Expected 'eq' | 'contains' | 'startsWith', received 'like'",
"correlationId": "b2f6c1e0-8f4a-4c8b-9d2e-1a2b3c4d5e6f",
"fields": [
{
"path": "body.filters.email.operator",
"code": "invalid_enum_value",
"message": "Invalid enum value. Expected 'eq' | 'contains' | 'startsWith', received 'like'"
}
]
}
| Field | Type | Description |
|---|---|---|
error | string | validation_failed for any field-level 400 (schema or domain) |
fields | array | One entry per failing field: path (dot-notation location prefixed with the request part — body., query., path., or header.), code (machine-readable cause, e.g. invalid_type, unrecognized_keys, too_small, unknown_custom_field, invalid_custom_field_value, duplicate), and a human-readable message |
validationDetail | string | All field failures joined into one string (same content as fields, flattened) |
Request bodies are validated strictly — unknown keys are rejected, not silently dropped. A mis-cased key is named, and when it case-insensitively matches a real field the response suggests the correct spelling:
{
"path": "body.filters.ExternalUserID",
"code": "unrecognized_keys",
"message": "Unrecognized key 'ExternalUserID' — did you mean 'externalUserId'?"
}
Domain errors that aren't tied to a single field (e.g. paying an already-settled
invoice) use the same envelope without fields[] — statusCode, error (the
status-derived code, e.g. conflict or unprocessable_entity), message, and
correlationId. See Payment Errors below.
HTTP Status Codes
Success Codes
| Code | Meaning | Description |
|---|---|---|
200 | OK | Request succeeded |
201 | Created | Resource created successfully |
Client Error Codes
| Code | Meaning | Common Causes |
|---|---|---|
400 | Bad Request | Missing required fields, invalid field values, malformed JSON |
401 | Unauthorized | Missing or invalid API key, expired session token |
403 | Forbidden | Valid credentials but insufficient permissions |
404 | Not Found | Resource does not exist or belongs to a different company |
422 | Unprocessable Entity | Request is well-formed but semantically invalid (e.g., refunding a pending transaction) |
429 | Too Many Requests | You exceeded your account's rate limit or monthly quota, or the per-IP safeguard — see Rate Limiting |
Server Error Codes
| Code | Meaning | Description |
|---|---|---|
500 | Internal Server Error | Unexpected server error — contact support if persistent |
502 | Bad Gateway | Payment processor communication failure |
503 | Service Unavailable | Temporary service disruption |
Common Error Scenarios
Authentication Errors
Missing or invalid API key:
{
"statusCode": 401,
"message": "Invalid API key",
}
Expired or invalid session token:
{
"statusCode": 401,
"message": "Session expired. Please authenticate again.",
}
The exact
messagewording varies by endpoint — some routes return a generic"Unauthorized"for an expired, invalid, or missing session token. Always branch on the401status, not the message text.
Fix: Create a new session token using POST /api/auth/session.
Validation Errors
Missing required field:
{
"statusCode": 400,
"error": "validation_failed",
"message": "Request validation failed: body.customerId: Required",
"validationDetail": "body.customerId: Required",
"correlationId": "b2f6c1e0-8f4a-4c8b-9d2e-1a2b3c4d5e6f",
"fields": [
{ "path": "body.customerId", "code": "invalid_type", "message": "Required" }
]
}
Unknown / mis-cased key (strict validation):
{
"statusCode": 400,
"error": "validation_failed",
"message": "Request validation failed: body.filters.Email: Unrecognized key 'Email' — did you mean 'email'?",
"validationDetail": "body.filters.Email: Unrecognized key 'Email' — did you mean 'email'?",
"correlationId": "b2f6c1e0-8f4a-4c8b-9d2e-1a2b3c4d5e6f",
"fields": [
{ "path": "body.filters.Email", "code": "unrecognized_keys", "message": "Unrecognized key 'Email' — did you mean 'email'?" }
]
}
Payment Errors
Invalid payment method:
{
"statusCode": 422,
"message": "Payment method is inactive or expired",
}
Payment declined:
{
"statusCode": 422,
"message": "Transaction declined by processor",
}
Pay invoice rejected (PUT /invoices/:invoiceId/payment):
The pay-invoice endpoint returns a 400 with a machine-readable reasonCode so
you can branch without string-matching the message. Branch on typeof reasonCode:
reasonCode | Type | Meaning |
|---|---|---|
1616, 1622, … | number | Processor gateway decline (see reasonText, e.g. INSUFFICIENT_FUNDS). |
"OVER_PAYMENT" | string | The requested amount exceeds the invoice's remaining balance. |
"ALREADY_PROCESSING" | string | Nothing left to pay — the invoice is settled, or an in-flight ACH already covers the balance. |
"QUOTE_CHANGED" | string | A confirmed surcharge quote no longer matches the surcharge rules (the merchant's fee settings changed). Nothing was charged; quote the card again. |
"OUTCOME_UNKNOWN" | string | The charge was submitted but its outcome did not arrive in time. It may have gone through. Re-read the invoice (GET /invoices/:id) before charging again; never retry blind. |
{
"statusCode": 400,
"message": "Payment of 61 exceeds the remaining balance of 60 on this invoice",
"reasonCode": "OVER_PAYMENT"
}
Size each payment to the invoice's amountDue (re-read via GET /invoices/:id
before charging) to avoid the OVER_PAYMENT rejection entirely.
Rate Limiting
Rate limit exceeded:
{
"statusCode": 429,
"message": "Too Many Requests"
}
Rate limits are enforced per account — your usage is metered on its own, so another client's traffic never affects yours (and yours never affects theirs). The default limits are:
| Limit | Value |
|---|---|
| Requests per second | 25 (sustained) |
| Burst | 50 |
| Monthly quota | 2,000,000 requests |
Limits apply to your account only. Exceed the per-second/burst rate and you get a 429 immediately; exceed the monthly quota and requests are rejected until it resets at the start of the next month. Retry 429s with exponential backoff — see Retry Strategy below.
Need higher limits? Contact support — higher-volume tiers are available. Postback traffic from payment processors does not count against your quota.
Per-IP safeguard
Separately from your account's limit, a network-level safeguard limits each client IP address:
| Requests from one IP address | Limit |
|---|---|
| Any API endpoint | about 300 per second, averaged over 5 minutes |
| Unauthenticated hosted-page endpoints (hosted payment, invoice and subscription setup pages, login, verification) | about 10 per second, averaged over 5 minutes |
The first limit is above every account tier, so one account's traffic stays under it even when all of it comes from a single IP address. If several accounts share one outbound IP address (for example, a platform calling on behalf of many merchants), their combined traffic counts toward that IP's limit; contact support if that applies to you. If a safeguard does trip, you get the same 429 body as above with a Retry-After header giving the seconds to wait. Honor Retry-After when it's present; otherwise back off exponentially.
Best Practices
Retry Strategy
Use exponential backoff for retryable errors:
- Node.js
- Python
async function apiCall(fn, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
const status = error.response?.status;
// Don't retry client errors (except rate limiting)
if (status >= 400 && status < 500 && status !== 429) {
throw error;
}
if (attempt === maxRetries) throw error;
const delay = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
// Usage
const invoice = await apiCall(() =>
axios.post("https://dev.payconnect.us/api/invoices", data, { headers })
);
import time
import requests
def api_call(fn, max_retries=3):
for attempt in range(max_retries + 1):
try:
response = fn()
response.raise_for_status()
return response
except requests.exceptions.HTTPError as e:
status = e.response.status_code
# Don't retry client errors (except rate limiting)
if 400 <= status < 500 and status != 429:
raise
if attempt == max_retries:
raise
delay = (2 ** attempt)
time.sleep(delay)
# Usage
invoice = api_call(lambda: requests.post(
"https://dev.payconnect.us/api/invoices",
json=data, headers=headers
))
Error Handling Checklist
- Check the status code — distinguish between client errors (fix your request) and server errors (retry)
- Read the message — error messages describe exactly what went wrong
- Handle token expiration — refresh session tokens when you receive a
401 - Respect rate limits — back off when you receive a
429 - Log correlation IDs — every error body echoes a
correlationId; log it (or send your own via thex-correlation-idrequest header) for easier debugging with support - Don't retry
4xxerrors — except for429, client errors require fixing the request
Using Correlation IDs
Include a x-correlation-id header to trace requests through the system:
curl -X POST https://dev.payconnect.us/api/invoices \
-H "x-session-token: YOUR_SESSION_TOKEN" \
-H "x-correlation-id: my-unique-request-id-123" \
-H "Content-Type: application/json" \
-d '{ ... }'
If you don't provide one, PayConnect generates a correlation ID automatically. Include this ID when contacting support about specific requests.
Next Steps
- Authentication — session token management
- Testing — test your integration in sandbox
- API Reference — full endpoint documentation