Troubleshooting
This page covers concrete debugging strategies for the integration paths that aren't always obvious. If you hit something not listed here, email support@ghostswap.io with the response X-Request-Id header.
"Invalid pair: x-y not available or temporary disabled" on swap creation
You'll see this as:
{ "error": { "type": "validation_error", "code": "invalid_request",
"message": "Invalid pair: btc-eth not available or temporary disabled",
"upstream_code": -32602 } }The literal text isn't always the literal cause. This message wraps several different root causes — the underlying error code (-32602) is "invalid params" at the JSON-RPC layer, and the platform sometimes serves this generic message even when the real issue is something else.
Most common actual causes (in order of likelihood):
- The
addressorrefundAddressfailed the strict creation-time validator. Even thoughPOST /v1/addresses/validateaccepted the address, the swap-creation path runs a stricter chain-specific validator. Try a different address format:- BTC: try a legacy
1…/3…address instead of a bech32bc1…(or vice versa) - ETH: try a checksummed mixed-case address (
0xAbCd…) instead of all-lowercase
- BTC: try a legacy
amountFromhas too many decimal places for the source chain's precision. Try fewer decimals (e.g.0.001instead of0.00100012).- Genuine temporary disable of the pair. Rare for major pairs (BTC↔ETH, ETH↔USDT, etc.). Try a different pair to rule out a global account issue.
Debug recipe — strip the swap to its minimum
// Try with NO refundAddress to isolate which field is rejected.
const res = await fetch(`${BASE}/v1/swaps`, {
method: 'POST',
headers: {
'Authorization': AUTH,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
from: 'btc', to: 'eth',
amountFrom: '0.001', // round number, well above min
address: '0xKnownGoodEthAddr', // an ETH address you control
// NO refundAddress
}),
});If this succeeds → the refund address was the issue. Try a different format. If this fails with the same error → it's the payout address or the amount.
/v1/addresses/validate accepts but /v1/swaps rejects the same address
Known behavior. The two validators are different:
POST /v1/addresses/validateruns a permissive chain-format check — basically "is this a syntactically valid address for this chain?"POST /v1/swapsruns a stricter check internally that rejects addresses that parse but won't actually work for the on-chain payout (e.g. addresses with the wrong checksum, or for the wrong network within a chain family).
Don't treat /v1/addresses/validate as proof the swap will succeed. Treat it as inline UX feedback during typing — fast and lenient. The real verdict comes at POST /v1/swaps.
Verifying a swap was actually created
When POST /v1/swaps returns HTTP 201 with a swap object, the swap is real. Your own DB or dashboard may lag — the source of truth is GET /v1/swaps/:id:
curl https://partners-api.ghostswap.io/v1/swaps/htpi6bqnazl7hbjd \
-H "Authorization: Bearer $TOKEN"If it returns the swap row, the swap exists in our system and the upstream liquidity layer.
For the list view of your org's swaps:
curl https://partners-api.ghostswap.io/v1/swaps?limit=20 \
-H "Authorization: Bearer $TOKEN"Where do I see the dashboard for my swaps?
The GhostSwap partner dashboard at /dashboard/transactions shows every swap created with credentials owned by your organization. Auto-refreshes every 10 seconds; flashes rows whose status changed.
Your partner-fee percent is locked at application time and is already factored into the amountUserReceives field returned by POST /v1/quotes. You don't need to add it on top — what the quote shows is what your end-user receives.
"I'm getting 503 upstream_not_configured"
Server-side: a liquidity-key configuration issue we need to resolve on our end. Email support@ghostswap.io with the X-Request-Id — there's no client-side workaround.
In the meantime, you can build the rest of your integration (UI, polling logic, error handling) against the read endpoints (GET /v1/currencies, GET /v1/pairs, POST /v1/addresses/validate) — those work independent of liquidity-key activation.
"I'm getting 503 provider_credential_pending"
Your account is still being activated by GhostSwap. The error message is:
Your account is still being activated. Currencies, pairs, and address validation are available now; fee-sensitive quotes and swap creation will be enabled within 1-3 business days.
This means:
- Currency, pair, and address-validation endpoints remain useful while you build
POST /v1/quotesandPOST /v1/swapsare blocked until activation finishes- Typical wait: 1-3 business days after admin approval
What to do:
- Build the rest of your integration (UI, polling logic, error handling) using currencies, pairs, and address validation while fee-sensitive quotes/swaps wait for activation
- Watch your dashboard at
/dashboard/api-credentials— when the activation lands, the next/v1/swapsPOST will succeed
If it's been more than 24 hours since approval, email support@ghostswap.io (or ping us on Telegram) with your X-Request-Id.
"I'm getting 502 upstream_bad_response"
The liquidity provider returned a non-JSON response — usually a transient CDN hiccup at their edge.
On read endpoints (/v1/currencies, /v1/pairs, /v1/quotes, GET /v1/swaps/:id): safe to retry once after a short backoff. These are stateless on our side.
On POST /v1/swaps: do not auto-retry blind. To prevent the rare case where the upstream may have accepted the swap before failing to return a parseable response, we fail the call immediately and do not retry internally on this path. Your retry-with-the-same-Idempotency-Key would hit our cache miss and could create a second upstream swap that we don't yet have a row for. Instead:
- Call
GET /v1/swaps?limit=20and check whether a swap with yourpartnerReferenceIdis already present. - If yes, treat that swap as the canonical one — display its
payinAddressto the user. - If no swap exists after ~30 seconds, retry with a new
Idempotency-Key. - If the problem persists or you're unsure, email support with your
X-Request-Idbefore retrying.
Idempotency key — when does it actually save you?
Idempotency-Key (UUID v4 on POST /v1/swaps) protects against duplicate swap creation in three concrete situations:
- Network retry: your fetch fails with a transient error; you retry with the same key — get the same swap back, no duplicate.
- Concurrent click: user clicks "Confirm" twice in quick succession (e.g., laggy UI). If both requests carry the same key, both return the same swap.
- Process crash mid-call: your service crashes between sending the request and storing the response. On restart, you can replay with the same key and recover.
The key must be the same across retries of the same logical attempt. Generate it once per "Confirm click" and store it before sending. Don't generate a new UUID inside the retry loop.
If you reuse the same Idempotency-Key with a different request body, you get HTTP 409 conflict — that's a guard against accidental key reuse with mismatched data.
429 rate-limited — retry strategy
HTTP/1.1 429 Too Many Requests
Retry-After: 1
Read Retry-After (seconds) from the response header. Wait that long, then retry with the same Idempotency-Key (if it was a swap creation). Two limits apply — 120 RPS per source IP and 30 RPS per credential — and separate credentials never share a quota.
If you see 429 on read endpoints (/v1/currencies, etc.), back off. These calls are cheap to cache (5–10 min for currencies).
"I lost my secret"
You have two paths:
You think the secret is just lost (not leaked) — visit /dashboard/api-credentials, find the credential row, click Reveal secret. The secret is shown again next to the always-visible public key. Copy both, store securely, click Hide.
You think the secret may have leaked (committed to a repo, posted in chat, etc.) — revoke instead. Click Revoke on the credential row → it's invalidated everywhere within seconds. Then click Create live credential for a new pair.
If a credential was created before recovery was supported (early-stage credentials), Reveal returns "This credential was created before secret recovery was enabled. Revoke it and create a new one to get a recoverable secret." — follow the revoke + reissue path.
Every Reveal action is audit-logged on our side.
Common confusion: amountTo vs amountUserReceives
Always show amountUserReceives to the user — it equals amountTo - networkFee and is the realistic estimate. Showing raw amountTo overstates the user's payout by the network fee.
Currency icons that 404
Some currencies have image: null in the response. Always wrap your icon render in a fallback:
{currency.image
? <img src={currency.image} alt="" onError={(e) => e.target.style.display = 'none'} />
: <span className="ticker-fallback">{currency.ticker[0].toUpperCase()}</span>}See also
- Errors — full error type matrix
- Idempotency — semantics and retry patterns
- Status lifecycle — every state transition
- End-to-end swap guide — happy-path implementation