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):

  1. The address or refundAddress failed the strict creation-time validator. Even though POST /v1/addresses/validate accepted 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 bech32 bc1… (or vice versa)
    • ETH: try a checksummed mixed-case address (0xAbCd…) instead of all-lowercase
  2. amountFrom has too many decimal places for the source chain's precision. Try fewer decimals (e.g. 0.001 instead of 0.00100012).
  3. 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/validate runs a permissive chain-format check — basically "is this a syntactically valid address for this chain?"
  • POST /v1/swaps runs 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/quotes and POST /v1/swaps are 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/swaps POST 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:

  1. Call GET /v1/swaps?limit=20 and check whether a swap with your partnerReferenceId is already present.
  2. If yes, treat that swap as the canonical one — display its payinAddress to the user.
  3. If no swap exists after ~30 seconds, retry with a new Idempotency-Key.
  4. If the problem persists or you're unsure, email support with your X-Request-Id before 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:

  1. Network retry: your fetch fails with a transient error; you retry with the same key — get the same swap back, no duplicate.
  2. 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.
  3. 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