Error Response Format
All API errors follow a consistent JSON structure:
{
"error": {
"code": "invalid_amount",
"message": "Amount must be a positive integer in cents.",
"details": {
"field": "amount",
"provided": -500,
"expected": "positive integer"
}
}
}| Field | Type | Description |
|---|---|---|
| error.code | string | Machine-readable error code |
| error.message | string | Human-readable error description |
| error.details | object? | Optional additional context about the error |
HTTP Status Codes
| Code | Status | Description |
|---|---|---|
| 400 | Bad Request | The request body is malformed or missing required fields |
| 401 | Unauthorized | Missing or invalid API key / JWT token |
| 403 | Forbidden | API key lacks permission for this action |
| 404 | Not Found | The requested resource does not exist |
| 409 | Conflict | Resource already exists or state conflict (e.g., duplicate idempotency key) |
| 412 | Precondition Failed | Payment cannot transition to the requested state |
| 422 | Unprocessable Entity | Request is well-formed but contains invalid values |
| 423 | Locked | Customer account is blocked (CoinFlow fraud detection) |
| 428 | Precondition Required | Customer must re-verify identity (CoinFlow re-verification) |
| 429 | Too Many Requests | Rate limit exceeded (100 requests/minute per API key) |
| 451 | Unavailable For Legal Reasons | KYC/KYB verification required before processing |
| 500 | Internal Server Error | Unexpected error on our end. Contact support if it persists |
| 502 | Bad Gateway | Upstream payment processor is temporarily unavailable |
ACH-Specific Error Codes
These errors are returned when ACH transactions fail or are returned by the banking network:
| Code | ACH Return | Description |
|---|---|---|
| ach_insufficient_funds | R01 | Customer account has insufficient funds to cover the debit |
| ach_account_closed | R02 | The bank account has been closed |
| ach_unauthorized_debit | R07 / R10 | Customer claims the debit was not authorized |
| ach_account_frozen | R16 | The bank account has been frozen by the bank or a legal authority |
ACH returns can arrive up to 60 days after the original transaction. Always listen for ach.returned webhook events to handle late returns and reverse any fulfilled orders.
CoinFlow-Specific Errors
These errors originate from the CoinFlow payment processor and require specific handling:
423Customer Blocked
CoinFlow's fraud detection has flagged and blocked this customer. The customer cannot make further purchases until the block is resolved.
Action: Direct the customer to contact support. Do not retry automatically.
428Re-verification Required
The customer must complete additional identity verification before continuing. This is triggered by changes in transaction patterns or risk signals.
Action: Redirect the customer to the CoinFlow verification flow using the verification_url in the error details.
451KYC Required
Know Your Customer verification is required before processing this transaction. New customers or high-value transactions may trigger this requirement.
Action: Redirect the customer to complete KYC using the kyc_url in the error details. The payment can be retried after verification.
Error Handling Best Practices
Recommended Pattern
Use the error.code field for programmatic error handling, and display the error.message field to users. The error.details object provides additional context for debugging.
- ✓ Use error codes, not status codes: Multiple error types can share the same HTTP status code. Always check
error.codefor specific handling - ✓ Retry with backoff for 5xx errors: Server errors are usually transient. Retry with exponential backoff (1s, 2s, 4s, 8s, max 3 retries)
- ✓ Never retry 4xx errors blindly: Client errors require fixing the request before retrying (except 429, which should be retried after the rate limit resets)
- ✓ Log the full error response: Always log the complete error object including
detailsfor debugging - ✓ Handle 423, 428, 451 gracefully: These CoinFlow-specific errors require user action, not automatic retries
- ✓ Use idempotency keys: For POST requests, include an
Idempotency-Keyheader to safely retry failed requests without creating duplicates
try {
const payment = await hedge.payments.create({
amount: 5000,
currency: 'USD',
customer_email: 'customer@example.com'
})
} catch (err) {
switch (err.code) {
case 'invalid_amount':
// Fix the request parameters
console.error('Invalid amount:', err.details)
break
case 'rate_limit_exceeded':
// Wait and retry
const retryAfter = err.details?.retry_after || 60
await sleep(retryAfter * 1000)
break
case 'customer_blocked':
// Direct user to support
showMessage('Please contact support for assistance.')
break
case 'kyc_required':
// Redirect to KYC flow
window.location.href = err.details.kyc_url
break
default:
// Log and show generic error
console.error('Payment error:', err)
showMessage('Something went wrong. Please try again.')
}
}