Swaps
Three endpoints for the full swap lifecycle. Swap creation is idempotent — safe to retry.
Create a swap
/v1/swapsValidates the destination address, locks an indicative rate, creates the swap, and returns a deposit address you display to your user.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Authorization | string | required | Bearer `<public_key>:<secret>` |
Content-Type | string | required | `application/json` |
Idempotency-Key | string | required | UUID v4. Repeating returns the original swap, no duplicate created. See [Idempotency](/docs/concepts/idempotency). |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
from | string | required | Lowercase payin ticker. |
to | string | required | Lowercase payout ticker. |
amountFrom | string | required | Decimal string. Must be within the pair's `min`/`max`. |
address | string | required | User's payout (destination) wallet. |
refundAddress | string | optional | Optional. 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. |
partnerReferenceId | string | optional | Your internal order ID. Stored on the swap row for cross-reference. Up to 120 chars. |
mode | string | optional | Defaults 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:
| Field | Format | Use it for |
|---|---|---|
id | swp_* | All calls back into our API (GET /v1/swaps/:id, idempotency-key correlation, support escalation to support@ghostswap.io). Stable, partner-namespaced, always present. |
providerSwapId | hex string from our upstream provider | Internal 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
/v1/swaps/:idFetches 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
/v1/swapsPaginated, most-recent first, scoped to your organization.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit | integer | optional | 1–100. Default 10. |
offset | integer | optional | Pagination 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 / code | When |
|---|---|
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.