Errors
All non-2xx responses use the same envelope. Inspect error.type to decide how to recover.
Envelope
{
"error": {
"type": "validation_error",
"code": "missing_field",
"message": "from is required",
"param": "from"
}
}| Field | Type | Notes |
|---|---|---|
type | string | High-level category. Drives your retry strategy. |
code | string | Short machine code. Stable; safe to switch on. |
message | string | Human-readable explanation. May change wording over time. |
param | string | Present only on validation_error. The request field that's invalid. |
retry_after_ms | number | Present on rate_limit_error. How long to wait before retrying. |
upstream_code | number | Present on upstream_error when the upstream returned a numeric code. |
Every response also has an X-Request-Id header. Log it. Pass it back to GhostSwap support when escalating — we use it to find your request in our logs.
Error types
These are the values you'll see in error.type. Switch on these to drive recovery — they're stable.
| Type | HTTP | Recoverable? | What to do |
|---|---|---|---|
validation_error | 400 | No (without changing input) | Surface to the user; fix the input. Check error.param for the bad field. |
authentication_error | 401 | No (without new credentials) | Check the Authorization header is present and well-formed. Code may be unauthenticated or revoked_credential. |
authorization_error | 403 | Sometimes | Org isn't active yet, or you don't have access to this resource. Code is usually forbidden or org_not_active. |
not_found | 404 | No | Wrong id or it doesn't belong to your org |
conflict | 409 | Yes | Most commonly idempotency_key_mismatch — same Idempotency-Key reused with a different body. Change one. |
unprocessable | 422 | Yes (with different input) | Request was well-formed but rejected by a business rule. See error.code. |
rate_limit_error | 429 | Yes | Wait error.retry_after_ms (or the Retry-After header), then retry with the same Idempotency-Key. |
upstream_error | 502 / 503 | Yes (transient) | Backoff and retry. Often resolves in seconds. |
internal_error | 500 | Sometimes | Backoff and retry with the same idempotency key. Escalate with X-Request-Id if it persists. |
Common codes
error.code is the short machine-readable classifier under each type. These are stable and safe to switch on. Common ones you'll see:
| Code | Type | Meaning |
|---|---|---|
invalid_request | validation_error | Generic bad-shape body (see error.param) |
missing_field | validation_error | A required body/query field was absent |
invalid_currency | validation_error | Unknown ticker or currency is temporarily disabled |
amount_below_min | validation_error | amountFrom is below the pair's minimum |
amount_above_max | validation_error | amountFrom exceeds the pair's maximum |
invalid_address | validation_error | The destination address failed validation |
idempotency_key_mismatch | conflict | Same Idempotency-Key, different body |
pending_request_exists | conflict | A previous request of this kind is still in flight (e.g. payouts) |
unauthenticated | authentication_error | Missing, malformed, or expired bearer credential |
revoked_credential | authentication_error | Credential was revoked or its parent org was suspended |
forbidden | authorization_error | Generic access denied |
org_not_active | authorization_error | The credential's org is pending_review / suspended / rejected |
below_threshold | unprocessable | Payout amount below the $100 threshold |
insufficient_balance | unprocessable | Payout amount exceeds available balance |
rate_limited | rate_limit_error | Per-credential RPS exceeded — see Retry-After |
upstream_rate_limited | upstream_error | GhostSwap's upstream liquidity cap hit (rare; we shield against this) |
upstream_not_configured | upstream_error | Server-side configuration issue. Email support |
upstream_empty_quote | upstream_error | No quote returned — usually means pair is unavailable |
upstream_bad_response | upstream_error | Liquidity provider returned non-JSON. Auto-retried once internally before surfacing; if you see this, it survived the retry. Retry your request with the same Idempotency-Key |
upstream_unreachable | upstream_error | Network failure reaching the liquidity layer. Auto-retried once internally |
provider_credential_pending | upstream_error (503) | Your dedicated liquidity key is still being activated by our liquidity team (1–3 business days from approval). Reads work; swap creation will be enabled once activation completes. No action needed by you |
Recovery patterns
Retry on transient
async function retryable(fn) {
for (let i = 0; i < 3; i++) {
const res = await fn();
if (res.ok) return res.json();
const body = await res.json().catch(() => ({}));
if (body?.error?.type !== 'upstream_error' && body?.error?.type !== 'rate_limit_error') {
throw new Error(`${body?.error?.code}: ${body?.error?.message}`);
}
const retryAfter = Number(res.headers.get('Retry-After')) || 2 ** i;
await new Promise((r) => setTimeout(r, retryAfter * 1000));
}
throw new Error('exhausted retries');
}Surface validation cleanly
const body = await res.json();
if (body.error?.type === 'validation_error') {
showFieldError(body.error.param, body.error.message);
}Distinguish from upstream
if (body.error?.type === 'upstream_error') {
// Show a generic "exchange provider is unavailable" message;
// your user didn't do anything wrong.
}See also
- Idempotency — how to retry safely.
- Rate limits — pre-empting 429s.