Overview

The OpenFX v1 API uses HTTP status codes to indicate success or failure. There is no status envelope wrapper — successful responses return the resource directly, and error responses return a structured error object.
Every error response includes a requestId field. Include this value in any support requests to help the OpenFX team quickly locate and diagnose the issue.

Error Response Shape

All 4xx and 5xx responses return a consistent error envelope:
{
  "error": {
    "type": "invalid_request_error",
    "code": "insufficient_funds",
    "message": "The source account has insufficient funds to complete this payment.",
    "status": 422,
    "requestId": "req_01953e1a5f4b7b01",
    "retryable": false,
    "documentationUrl": "https://docs.openfx.com/guides/concepts/error-handling",
    "details": {},
    "fieldErrors": []
  }
}
FieldTypeRequiredDescription
typestringYesCategory of error. One of the 9 error types listed below.
codestringYesMachine-readable error code (e.g., insufficient_funds, quote_expired).
messagestringYesHuman-readable description of what went wrong.
statusintegerYesHTTP status code (mirrors the response status).
requestIdstringYesUnique identifier for this request (uses req_ prefix).
retryablebooleanYesWhether retrying the request with the same parameters could succeed.
documentationUrlstringNoLink to relevant documentation for this error.
detailsobjectNoAdditional context about the error.
fieldErrorsarrayNoPer-field validation errors. Present on 400 validation failures.

Error Types

Every error response includes a type field that categorizes the error:
TypeHTTP StatusDescription
invalid_request_error400Request body or query parameters are malformed or contain invalid values.
authentication_error401Authentication credentials are missing, invalid, or expired.
authorization_error403Valid credentials but insufficient permissions for this operation.
not_found_error404The requested resource does not exist or is not accessible.
conflict_error409State conflict — the resource is not in a valid state for this operation (e.g., payment already submitted).
idempotency_error409Idempotency key conflict — either reused with a different body or the original request is still in flight.
rate_limit_error429Too many requests. Slow down and respect the Retry-After header.
business_rule_error422The request is well-formed but violates a business rule (e.g., insufficient funds, KYB not completed).
api_error500Internal server error. These are always retryable.

HTTP Status Code Summary

StatusMeaningTypical Cause
200OKSuccessful GET, PATCH, or action.
201CreatedResource successfully created (POST).
202AcceptedRequest accepted for async processing (e.g., payment creation).
204No ContentSuccessful DELETE.
400Bad RequestValidation failure, malformed JSON, missing required fields.
401UnauthorizedMissing or invalid API key, bad signature, expired timestamp.
403ForbiddenValid credentials but insufficient permissions.
404Not FoundResource does not exist or you do not have access.
409ConflictState conflict or idempotency key conflict.
422Unprocessable EntityBusiness rule violation (e.g., insufficient funds).
429Too Many RequestsRate limit exceeded.
500Internal Server ErrorUnexpected server error. Always retryable.

Field Validation Errors

When a 400 invalid_request_error is caused by specific field-level issues, the fieldErrors array provides granular detail:
{
  "error": {
    "type": "invalid_request_error",
    "code": "validation_error",
    "message": "Request body contains invalid fields.",
    "status": 400,
    "requestId": "req_01953e1a5f4b7b03",
    "retryable": false,
    "fieldErrors": [
      {
        "field": "address.postalCode",
        "code": "required",
        "message": "Postal code is required for US addresses."
      },
      {
        "field": "sendAmount.amount",
        "code": "out_of_range",
        "message": "Amount must be greater than 0."
      }
    ]
  }
}
Each field error contains:
FieldTypeDescription
fieldstringDot-path to the invalid field (e.g., address.postalCode, sendAmount.currency).
codestringMachine-readable error code for programmatic handling.
messagestringHuman-readable description of the validation failure.

Field Error Codes

CodeDescription
requiredField is required but was not provided.
invalid_formatField value does not match the expected format (e.g., invalid email, bad date).
out_of_rangeNumeric value is outside the allowed range.
not_foundA referenced resource (by ID) does not exist.
already_existsA unique value is already in use (e.g., duplicate account number).
immutableField cannot be changed after creation.
too_longString value exceeds the maximum allowed length.
too_shortString value is shorter than the minimum required length.
invalid_enum_valueValue is not one of the allowed enum options.
mutually_exclusiveField cannot be provided alongside another specified field.

Example Error Responses

400 — Validation Error

{
  "error": {
    "type": "invalid_request_error",
    "code": "validation_error",
    "message": "Request body contains invalid fields.",
    "status": 400,
    "requestId": "req_01953e1a5f4b7b04",
    "retryable": false,
    "fieldErrors": [
      {
        "field": "rail",
        "code": "invalid_enum_value",
        "message": "Rail must be one of: ach, fedwire, swift, fednow, crypto, sepa, fps, open."
      }
    ]
  }
}

401 — Authentication Error

{
  "error": {
    "type": "authentication_error",
    "code": "invalid_signature",
    "message": "The request signature in the X-Signature header is invalid. Verify your signing key and algorithm.",
    "status": 401,
    "requestId": "req_01953e1a5f4b7b05",
    "retryable": false
  }
}

404 — Not Found

{
  "error": {
    "type": "not_found_error",
    "code": "payment_not_found",
    "message": "No payment found with ID pmt_01953e1a5f4b7999.",
    "status": 404,
    "requestId": "req_01953e1a5f4b7b06",
    "retryable": false
  }
}

409 — State Conflict

{
  "error": {
    "type": "conflict_error",
    "code": "payment_not_cancelable",
    "message": "This payment has already been completed and cannot be canceled.",
    "status": 409,
    "requestId": "req_01953e1a5f4b7b07",
    "retryable": false
  }
}

422 — Business Rule Error

{
  "error": {
    "type": "business_rule_error",
    "code": "insufficient_funds",
    "message": "The source account has insufficient funds to complete this payment.",
    "status": 422,
    "requestId": "req_01953e1a5f4b7b08",
    "retryable": false
  }
}

429 — Rate Limit Exceeded

{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Too many requests. Please retry after the delay indicated in the Retry-After header.",
    "status": 429,
    "requestId": "req_01953e1a5f4b7b09",
    "retryable": true
  }
}
The response includes a Retry-After header indicating how many seconds to wait:
HTTP/1.1 429 Too Many Requests
Retry-After: 30

Retry Strategy

Not all errors are retryable. Use the retryable field to determine whether to retry:
Error TypeRetryableStrategy
invalid_request_errorNoFix the request and resubmit.
authentication_errorNoCheck credentials and signing logic.
authorization_errorNoRequest appropriate permissions.
not_found_errorNoVerify the resource ID.
conflict_errorNoRe-read the resource state before retrying.
idempotency_error (in_flight)YesWait for Retry-After seconds, then retry.
idempotency_error (duplicate_key)NoGenerate a new idempotency key.
rate_limit_errorYesWait for Retry-After seconds, then retry.
business_rule_errorNoResolve the business condition (e.g., fund the account).
api_errorYesRetry with exponential backoff.
import time
import requests

def api_request_with_retry(method, url, headers, max_retries=3, **kwargs):
    """Make an API request with automatic retry for retryable errors."""
    for attempt in range(max_retries + 1):
        response = method(url, headers=headers, **kwargs)

        # Success
        if response.status_code < 400:
            return response

        error_body = response.json()
        error = error_body.get("error", {})

        # Check if retryable
        if not error.get("retryable", False) or attempt == max_retries:
            response.raise_for_status()

        # Respect Retry-After header if present
        retry_after = response.headers.get("Retry-After")
        if retry_after:
            wait = int(retry_after)
        else:
            # Exponential backoff: 1s, 2s, 4s
            wait = 2 ** attempt

        print(f"Retryable error ({error.get('code')}), "
              f"waiting {wait}s (attempt {attempt + 1}/{max_retries})")
        time.sleep(wait)

    return response

Best Practices

Always check the type field programmatically. Use type for broad error categorization and code for specific error handling logic. Use message for logging and display.
Log the requestId on every error. Include it in support tickets and internal monitoring dashboards. The requestId is the fastest way for the OpenFX support team to trace an issue.
Handle fieldErrors for form validation. Map field error dot-paths (e.g., address.postalCode) back to your UI form fields for inline error display.
Do not parse message for logic. The message text is human-readable and may change without notice. Always use type and code for programmatic decisions.