Swaps

Three endpoints for the full swap lifecycle. Swap creation is idempotent — safe to retry.

Create a swap

POST/v1/swaps

Validates the destination address, locks an indicative rate, creates the swap, and returns a deposit address you display to your user.

Headers

NameTypeRequiredDescription
AuthorizationstringrequiredBearer `<public_key>:<secret>`
Content-Typestringrequired`application/json`
Idempotency-KeystringrequiredUUID v4. Repeating returns the original swap, no duplicate created. See [Idempotency](/docs/concepts/idempotency).

Request body

NameTypeRequiredDescription
fromstringrequiredLowercase payin ticker.
tostringrequiredLowercase payout ticker.
amountFromstringrequiredDecimal string. Must be within the pair's `min`/`max`.
addressstringrequiredUser's payout (destination) wallet.
refundAddressstringoptionalOptional. Where funds return if the swap fails (e.g. amount below `min`, fixed-rate window expired). Must be on the `from` chain. Omitting it is fine — recovery in failure cases will fall back to the original sending address where the chain supports it; otherwise contact support.
partnerReferenceIdstringoptionalYour internal order ID. Stored on the swap row for cross-reference. Up to 120 chars.
modestringoptionalDefaults to `"float"`. `"fixed"` is on the [roadmap](/docs/roadmap) and returns 400.

Example

import { randomUUID } from 'node:crypto';
 
const res = await fetch(`${BASE}/v1/swaps`, {
  method: 'POST',
  headers: {
    'Authorization': AUTH,
    'Content-Type': 'application/json',
    'Idempotency-Key': randomUUID(),
  },
  body: JSON.stringify({
    from: 'btc',
    to: 'eth',
    amountFrom: '0.01',
    address: '0xUserPayoutAddress...',
    refundAddress: 'bc1qUserRefundAddress...',
    partnerReferenceId: 'order_42',
  }),
});
const { swap } = await res.json();
// Display swap.payinAddress to the user. They send swap.amountFrom there.

Response (HTTP 201)

{
  "swap": {
    "id": "swp_a1b2c3d4",
    "providerSwapId": "fa1980b980bb7640",
    "status": "waiting",
    "from": "btc",
    "to": "eth",
    "amountFrom": "0.01",
    "amountExpectedTo": "0.1532",
    "networkFee": "0.0021",
    "rate": "15.32",
    "payinAddress": "bc1qDepositAddressFromUs...",
    "payoutAddress": "0xUserPayoutAddress...",
    "refundAddress": "bc1qUserRefundAddress...",
    "partnerReferenceId": "order_42",
    "createdAt": "2026-04-29T12:00:00.000Z"
  }
}

About the two IDs

Each swap response carries two identifiers — use the right one for the right job:

FieldFormatUse it for
idswp_*All calls back into our API (GET /v1/swaps/:id, idempotency-key correlation, support escalation to support@ghostswap.io). Stable, partner-namespaced, always present.
providerSwapIdhex string from our upstream providerInternal reference we use to correlate against upstream records. Include it when escalating a stuck swap to support@ghostswap.io so we can look it up faster. Briefly null between create and the first upstream confirm (typically under one second); steady-state always populated.

If you store only one of the two on your end, store both. Saves you a lookup the day a partner asks "where is my transaction on the upstream side?"


Get a swap

GET/v1/swaps/:id

Fetches the current state of a swap. Use the id returned by the create response.

Example

const res = await fetch(`${BASE}/v1/swaps/swp_a1b2c3d4`, {
  headers: { 'Authorization': AUTH },
});
const { swap } = await res.json();
console.log(swap.status); // "waiting", "confirming", "finished", ...

Status updates flow from a background worker that polls upstream every ~30 seconds. Poll this endpoint at any cadence; we return our database state. See Status lifecycle.


List swaps

GET/v1/swaps

Paginated, most-recent first, scoped to your organization.

Query parameters

NameTypeRequiredDescription
limitintegeroptional1–100. Default 10.
offsetintegeroptionalPagination cursor.

Example

const res = await fetch(`${BASE}/v1/swaps?limit=50&offset=0`, {
  headers: { 'Authorization': AUTH },
});
const { swaps } = await res.json();

Response

{
  "swaps": [
    { "id": "swp_a1b2c3d4", "status": "finished", ... },
    { "id": "swp_99887766", "status": "confirming", ... }
  ]
}

Errors

Type / codeWhen
validation_error (missing_idempotency_key, HTTP 400)The Idempotency-Key header is required on POST /v1/swaps. Generate a UUID v4 once per Confirm action and reuse it on retries — never regenerate per HTTP attempt.
validation_error (amount_below_min, HTTP 400, param: amountFrom)amountFrom is below the pair's minimum. Error message includes the actual minimum. Pre-validate locally with /v1/pairs?from=…&to=… to avoid this on the swap-creation path.
validation_error (amount_above_max, HTTP 400, param: amountFrom)amountFrom is above the pair's maximum. Same pre-validation guidance as amount_below_min.
validation_error (pair_unsupported, HTTP 400, param: pair)The from/to pair is not currently available for float-rate swaps.
validation_error (field: 'mode')mode: 'fixed' — not supported in v1.
validation_error (field: 'address')Destination address failed upstream validation.
validation_error (other)Missing field, invalid currency, generic body validation failure.
authentication_error (unauthenticated, HTTP 401)Missing or invalid Authorization header. Also returned when the credential itself was revoked — issue a new one.
authorization_error (org_pending_review, HTTP 403)Your organization is still under admin review. The API will start accepting calls once approved. No action needed; you'll be emailed when ready.
authorization_error (org_suspended, HTTP 403)Your organization has been suspended. The credential itself is fine — contact support@ghostswap.io.
authorization_error (org_rejected, HTTP 403)Your partner application was not approved. Contact support@ghostswap.io for details.
not_found (HTTP 404)Swap id doesn't exist or doesn't belong to your org.
conflict (idempotency_key_mismatch, HTTP 409)Idempotency-Key reused with a different request body.
rate_limit_error (rate_limited, HTTP 429)Per-credential rate limit exceeded. Comes with Retry-After. See Rate limits for current enforcement status.
upstream_error (provider_credential_pending, HTTP 503)Your dedicated liquidity key is still being activated upstream. Reads work; swap creation enables once activation completes (typically 1–3 business days from approval). See Troubleshooting.
upstream_error (upstream_bad_response, HTTP 502)Liquidity provider returned non-JSON. Auto-retried once internally; surface only if both attempts failed. Retry with the same Idempotency-Key.
upstream_error (other)Generic liquidity-layer issue. Back off and retry with the same Idempotency-Key.

See Errors for the envelope shape and recovery strategy per type.