Errors

Every non-2xx response from the Hexolus Payment Gateway shares one envelope:

{
  "message": "human-readable description",
  "code": "machine-readable enum"
}

The envelope may also carry these optional fields:

Field When present
field_errors On 422 validation errors emitted by form-style endpoints; map of field โ†’ [reason].
action_hint On a small set of operator-facing errors; human remediation copy for the admin UI.

This page documents:


The error envelope

Always parse code (machine) first; the message field is meant for human display and may evolve. The Content-Type is always application/json for errors emitted by Hexolus's own code; an upstream load balancer or CDN returning a 502 may yield HTML, so check Content-Type defensively.

{
  "message": "notional_minor must be > 0",
  "code": "validation"
}

Error code reference

code HTTP status Retryable? Meaning
auth 401 no Missing, malformed, unknown, or expired bearer token. See Authentication.
forbidden 403 no Authenticated but the action is not permitted for your client. Rare on the public API.
not_found 404 no The resource ID does not exist OR belongs to another client. (See tenancy note.)
conflict 409 depends A uniqueness constraint was violated. Idempotency-key collisions are NOT this; see below.
validation 422 no The request body or query failed validation. Look at message and field_errors.
rate_limit 429 yes You exceeded a rate cap. Honour Retry-After if present.
network 502 yes A transient network failure reaching Xendit. Retry with backoff.
server_error 502 yes Xendit returned a 5xx. Retry with backoff.
internal_error 500 maybe Unexpected Hexolus-side failure. Retry once with the same Idempotency-Key; if it persists, contact support.

Codes not listed above are present in Hexolus's internal taxonomy (ip_not_allowed, domain_taken, transfer_locked, auth_code_invalid, insufficient_balance, ineligible_registrant) but are emitted only by internal domain / billing flows and will not appear on the payment gateway public API. If you ever do see one, treat it as an internal_error.


HTTP status mapping

The mapping is deterministic and exhaustive:

HTTP status Possible code values
401 auth
403 forbidden
404 not_found
409 conflict
422 validation (most common), auth_code_invalid
429 rate_limit
500 internal_error
502 network, server_error

If you write a generic error handler, switching on code is more forward-compatible than switching on HTTP status.


Tenancy and 404

Hexolus deliberately does not distinguish "this resource does not exist" from "this resource exists but belongs to another tenant". Both surface as 404 not_found. This prevents enumeration attacks where a caller could otherwise learn which IDs are in use across the platform by counting 403s vs 404s.

This applies to:

If you are confident the ID was minted by your own client and you receive a 404, the most likely cause is a typo or a misrouted environment (production ID against staging or vice versa).


Idempotency conflicts

The Idempotency-Key header on POST /v1/payments is not a uniqueness constraint that returns 409 conflict. Instead:

The practical implication: choose Idempotency-Key values that are provably unique per logical request (e.g. order-{your_uuid}, order-{date}-{seq}). If your key generator might collide, you will silently receive someone else's payment instead of a clean 409.

If you genuinely need to detect a collision client-side, store the (idempotency_key, payment_id) pair locally and reject any reuse before sending the request.


Debugging 422 validation errors

A 422 validation response always includes a message that names the failed rule:

{
  "message": "channel_code required for method virtual_account",
  "code": "validation"
}

For form-style endpoints (admin and a subset of client API surfaces), an additional field_errors block points at the offending field(s):

{
  "message": "request body is invalid",
  "code": "validation",
  "field_errors": {
    "amount": ["must be > 0"],
    "channel_code": ["unsupported value: \"BCASYARIAH\""]
  }
}

Common 422 causes on the payments API:

message Fix
notional_minor must be > 0 Send a positive integer in amount.
unsupported method: <x> Use virtual_account, ewallet, or qris.
channel_code required for method <x> VA and e-wallet require channel_code. QRIS does not.
invalid JSON body: ... The body did not parse as JSON, or contained an unknown field (strict mode).
metadata is not JSON-serialisable: ... The supplied metadata had a non-JSON value (e.g. NaN).
payment cannot be cancelled in status=<x> Cancel is only valid while status == "pending".

For anything still unclear, copy the full response (including the X-Hexolus-Delivery-Id header if from a webhook) and contact support โ€” Hexolus retains request logs for 14 days.