Skip to main content
A sell moves value out of the user’s Uphold crypto account to a fiat external account (card via OCT or bank).

Prerequisites

  • The user has completed onboarding.
  • The user has an internal Uphold crypto account with sufficient balance.
  • Capabilities — card payouts require the card-withdrawals capability and a destination card with octSupport: "supported"; the crypto → fiat leg of a bank sell requires trades. Bank withdrawal capability varies by rail — see the per-rail guides.
  • The integration uses Uphold’s rails. Partner-owned rails (omnibus) follow a different flow not covered here.

Walkthrough

Prefer to click through the flow? See the interactive sell walkthrough.

Select a crypto to sell

Call List accounts to retrieve the user’s Uphold accounts. Present crypto accounts with a non-zero balance as sellable.
GET /core/accounts
{
  "accounts": [
    {
      "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8",
      "asset": "USD",
      "balance": "0.00",
      "status": "ok"
    },
    {
      "id": "f8c2d1e4-3a7b-4c9d-8e5f-2b1a6d3c7e8f",
      "asset": "BTC",
      "balance": "0.1",
      "status": "ok"
    },
    {
      "id": "c11608ff-739d-4f38-bf91-f2d51c3b9c19",
      "asset": "EUR",
      "balance": "0.00",
      "status": "ok"
    }
  ]
}
The chosen account (f8c2d1e4 / BTC in this example) is the origin for the sell.

Define the destination

The destination determines where the sell proceeds go. Choose the path that matches the user’s intent:

Fiat external account

The user receives the proceeds in a linked card or bank account.
Apple Pay and Google Pay are not supported as sell destinations — the platforms do not expose a payout API. Card OCT is the only card-rail payout supported.

Check available rails

Call List Rails to verify the payout rail you plan to use is available. The withdraw feature must be present for the rail to be usable as a sell destination. Card:
GET /core/rails?type=card
{
  "rails": [
    {
      "type": "card",
      "network": "visa",
      "method": "debit-card",
      "asset": "USD",
      "decimals": 2,
      "features": ["deposit", "withdraw"]
    }
  ]
}
Bank (example: SEPA):
GET /core/rails?type=bank&network=sepa&asset=EUR
{
  "rails": [
    {
      "type": "bank",
      "network": "sepa",
      "method": "bank-transfer",
      "asset": "EUR",
      "decimals": 2,
      "features": ["deposit", "withdraw"]
    }
  ]
}
Let the user link a new external account or select an existing one. Card: Call List external accounts to fetch the user’s saved cards.
GET /core/external-accounts
Make sure the selected card has status: "ok" and in features.
{
  "externalAccounts": [
    {
      "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7",
      "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a",
      "type": "card",
      "status": "ok",
      "label": "My Visa Card",
      "asset": "GBP",
      "network": "visa",
      "features": [
        "deposit",
        "withdraw"
      ],
      "details": {
        "type": "debit",
        "last4Digits": "5119",
        "expiryDate": {
          "month": 12,
          "year": 2028
        },
        "octSupport": "supported"
      }
    }
  ]
}
If the user wants to use a card not yet on file, call Create external account with the card details.
POST /core/external-accounts
{
  "type": "card",
  "label": "My Visa Card",
  "number": "4921817844445119",
  "securityCode": "123",
  "expiryDate": {
    "month": 12,
    "year": 2028
  }
}
The response initially returns status: "processing" while the card is validated.
{
  "externalAccount": {
    "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7",
    "type": "card",
    "status": "processing",
    "label": "My Visa Card"
  }
}
Once validated, the status transitions to ok and the full details are available.
{
  "externalAccount": {
    "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7",
    "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a",
    "type": "card",
    "status": "ok",
    "label": "My Visa Card",
    "asset": "GBP",
    "network": "visa",
    "features": [
      "deposit",
      "withdraw"
    ],
    "details": {
      "type": "debit",
      "last4Digits": "5119",
      "expiryDate": {
        "month": 12,
        "year": 2028
      },
      "octSupport": "supported"
    }
  }
}
Monitor the external account status via Get external account or the external-account.status-changed webhook until status is ok before proceeding.

EMD disclaimer

This disclaimer is required to comply with FCA regulations. Display it to users based in GB, the first time they link a credit/debit card or generate bank deposit details — it only needs to be shown once:
Uphold Europe Limited is an EMD Agent of Optimus Cards UK Limited (FRN: 902034). All received funds are held in a designated safekeeping account with a regulated bank and kept separate from Uphold’s own assets. These funds are not protected by the UK Financial Services Compensation Scheme.
A freshly linked card returns "octSupport": "unknown" while Uphold verifies OCT eligibility. Poll GET /core/external-accounts/{id} until details.octSupport is "supported" before using the card as a sell destination.
Bank: Bank account linking varies by rail. See the per-rail guides: Once the user has a linked bank external account, retrieve it with List external accounts. Filter for type: "bank", status: "ok", and "withdraw" in features.
GET /core/external-accounts
{
  "externalAccounts": [
    {
      "id": "cc8e8abc-0f95-4b9f-8490-2569a67bd80e",
      "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604",
      "type": "bank",
      "status": "ok",
      "label": "My SEPA Bank Account",
      "asset": "EUR",
      "network": "sepa",
      "features": ["withdraw"],
      "details": {
        "bic": "EXAAAT2K",
        "iban": "AT48444441229809844"
      },
      "createdAt": "2025-03-10T09:03:45.321Z",
      "updatedAt": "2025-03-10T09:03:45.321Z"
    }
  ]
}
Also ensure the user has a fiat Uphold account in the bank’s currency to receive the trade proceeds. If one does not exist, create it with Create account.
POST /core/accounts
{
  "label": "My EUR Account",
  "asset": "EUR"
}

Transaction preview

Show the user a preview of the sell before committing. The preview is a quote — a price-locked offer valid until expiresAt. The amount the user specifies is denomination.amount; set denomination.asset to indicate whether the amount is expressed in the source or destination asset.
Quotes expire quickly. Capture user confirmation before expiresAt and re-quote if the user hesitates.

Fiat external account

Card (OCT): A card sell settles in a single transaction — the crypto account is debited, converted at the locked rate, and fiat is paid out to the card via OCT.
POST /core/transactions/quote
{
  "origin": {
    "type": "account",
    "id": "f8c2d1e4-3a7b-4c9d-8e5f-2b1a6d3c7e8f"
  },
  "destination": {
    "type": "external-account",
    "id": "aa6e6efa-8d73-497c-8278-0347f459bd68"
  },
  "denomination": {
    "asset": "USD",
    "amount": "490.00",
    "target": "destination"
  }
}
{
  "quote": {
    "id": "a7b8c9d0-e1f2-4a3b-4c5d-e6f7a8b9c0d1",
    "origin": {
      "amount": "0.0049",
      "asset": "BTC",
      "rate": "0.00001",
      "node": {
        "type": "account",
        "id": "f8c2d1e4-3a7b-4c9d-8e5f-2b1a6d3c7e8f",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "destination": {
      "amount": "490.00",
      "asset": "USD",
      "rate": "1",
      "node": {
        "type": "external-account",
        "id": "aa6e6efa-8d73-497c-8278-0347f459bd68",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "USD",
      "amount": "490.00",
      "target": "destination",
      "rate": "0.00001"
    },
    "fees": [],
    "expiresAt": "2026-06-01T13:00:30Z"
  }
}
Bank: A bank sell runs in two sequenced legs: a crypto-to-fiat trade lands fiat in the user’s Uphold fiat account, followed by a fiat withdrawal to the linked bank account. Create a quote for each leg. Trade quote (crypto → fiat):
POST /core/transactions/quote
{
  "origin": {
    "type": "account",
    "id": "f8c2d1e4-3a7b-4c9d-8e5f-2b1a6d3c7e8f"
  },
  "destination": {
    "type": "account",
    "id": "c11608ff-739d-4f38-bf91-f2d51c3b9c19"
  },
  "denomination": {
    "asset": "EUR",
    "amount": "250.00",
    "target": "destination"
  }
}
{
  "quote": {
    "id": "b8c9d0e1-f2a3-4b4c-5d6e-f7a8b9c0d1e2",
    "origin": {
      "amount": "0.005",
      "asset": "BTC",
      "rate": "0.00002",
      "node": {
        "type": "account",
        "id": "f8c2d1e4-3a7b-4c9d-8e5f-2b1a6d3c7e8f",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "destination": {
      "amount": "250.00",
      "asset": "EUR",
      "rate": "1",
      "node": {
        "type": "account",
        "id": "c11608ff-739d-4f38-bf91-f2d51c3b9c19",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "EUR",
      "amount": "250.00",
      "target": "destination",
      "rate": "0.00002"
    },
    "fees": [],
    "expiresAt": "2026-06-01T13:05:30Z"
  }
}
Withdrawal quote (fiat → bank): After presenting the trade quote, also create the withdrawal quote so the user sees the full sell preview before confirming. The example below uses SEPA (EUR). For rail-specific quote shapes, see the per-rail bank-transfer withdrawal guides.
POST /core/transactions/quote
{
  "origin": {
    "type": "account",
    "id": "c11608ff-739d-4f38-bf91-f2d51c3b9c19"
  },
  "destination": {
    "type": "external-account",
    "id": "cc8e8abc-0f95-4b9f-8490-2569a67bd80e"
  },
  "denomination": {
    "asset": "EUR",
    "amount": "250.00",
    "target": "origin"
  }
}
{
  "quote": {
    "id": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0",
    "origin": {
      "amount": "250.00",
      "asset": "EUR",
      "rate": "1",
      "node": {
        "type": "account",
        "id": "c11608ff-739d-4f38-bf91-f2d51c3b9c19",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "destination": {
      "amount": "250.00",
      "asset": "EUR",
      "rate": "1",
      "node": {
        "type": "external-account",
        "id": "cc8e8abc-0f95-4b9f-8490-2569a67bd80e",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "EUR",
      "amount": "250.00",
      "target": "origin",
      "rate": "1"
    },
    "fees": [],
    "expiresAt": "2026-06-01T13:06:30Z"
  }
}

Confirm quote

Commit the sell by calling Create transaction with the quote ID.

Fiat external account

Card (OCT): No returnUrl or 3DS is needed — card sell transactions do not require user card authorization.
POST /core/transactions
{
  "quoteId": "a7b8c9d0-e1f2-4a3b-4c5d-e6f7a8b9c0d1"
}
{
  "transaction": {
    "id": "c9d0e1f2-a3b4-4c5d-6e7f-a8b9c0d1e2f3",
    "status": "processing",
    "origin": {
      "amount": "0.0049",
      "asset": "BTC",
      "rate": "0.00001",
      "node": {
        "type": "account",
        "id": "f8c2d1e4-3a7b-4c9d-8e5f-2b1a6d3c7e8f",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "destination": {
      "amount": "490.00",
      "asset": "USD",
      "rate": "1",
      "node": {
        "type": "external-account",
        "id": "aa6e6efa-8d73-497c-8278-0347f459bd68",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "USD",
      "amount": "490.00",
      "target": "destination",
      "rate": "0.00001"
    },
    "fees": [],
    "quotedAt": "2026-06-01T13:00:00Z",
    "createdAt": "2026-06-01T13:00:05Z",
    "updatedAt": "2026-06-01T13:00:05Z"
  }
}
Wait for the core.transaction.status-changed webhook with status: "completed" before notifying the user. Bank — trade leg (crypto → fiat):
POST /core/transactions
{
  "quoteId": "b8c9d0e1-f2a3-4b4c-5d6e-f7a8b9c0d1e2"
}
{
  "transaction": {
    "id": "d0e1f2a3-b4c5-4d6e-7f8a-b9c0d1e2f3a4",
    "status": "processing",
    "origin": {
      "amount": "0.005",
      "asset": "BTC",
      "rate": "0.00002",
      "node": {
        "type": "account",
        "id": "f8c2d1e4-3a7b-4c9d-8e5f-2b1a6d3c7e8f",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "destination": {
      "amount": "250.00",
      "asset": "EUR",
      "rate": "1",
      "node": {
        "type": "account",
        "id": "c11608ff-739d-4f38-bf91-f2d51c3b9c19",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "EUR",
      "amount": "250.00",
      "target": "destination",
      "rate": "0.00002"
    },
    "fees": [],
    "quotedAt": "2026-06-01T13:05:00Z",
    "createdAt": "2026-06-01T13:05:05Z",
    "updatedAt": "2026-06-01T13:05:05Z"
  }
}
UK retail users may encounter a 409 user_capability_failure with restrictions: ["financial-promotion-cooldown-running"] for 24 hours after onboarding. See the trade guide for cooldown details.
Wait for the core.transaction.status-changed webhook with status: "completed" before proceeding to the withdrawal leg. Bank — withdrawal leg (fiat → bank): Once the trade settles and fiat lands in the user’s Uphold fiat account, confirm the withdrawal quote.
POST /core/transactions
{
  "quoteId": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0"
}
{
  "transaction": {
    "id": "d3e4f5a6-1b2c-4d5e-9f8a-7b6c5d4e3f2a",
    "status": "processing",
    "origin": {
      "amount": "250.00",
      "asset": "EUR",
      "rate": "1",
      "node": {
        "type": "account",
        "id": "c11608ff-739d-4f38-bf91-f2d51c3b9c19",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "destination": {
      "amount": "250.00",
      "asset": "EUR",
      "rate": "1",
      "node": {
        "type": "external-account",
        "id": "cc8e8abc-0f95-4b9f-8490-2569a67bd80e",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "EUR",
      "amount": "250.00",
      "target": "origin",
      "rate": "1"
    },
    "fees": [],
    "quotedAt": "2026-06-01T13:06:00Z",
    "createdAt": "2026-06-01T13:06:05Z",
    "updatedAt": "2026-06-01T13:06:05Z"
  }
}
Bank withdrawals settle asynchronously — instantly for FPS / FedNow, hours to days for ACH / SEPA. See the per-rail bank-transfer withdrawal guides for timing details.
Wait for the core.transaction.status-changed webhook with status: "completed" before notifying the user.

Monitor for settlement

Monitor each transaction in the chain by listening for core.transaction.status-changed webhooks or polling. Each leg fires its own event — wait for status: "completed" on one leg before submitting the next.

Via webhooks

Subscribe to core.transaction.status-changed. The event fires on every status transition; filter on data.transaction.status to detect terminal states:
  • completed — transaction settled successfully
  • failed — transaction failed; inspect data.transaction.errors for details
{
  "id": "f4a5b6c7-d8e9-4f0a-1b2c-d3e4f5a6b7c8",
  "type": "core.transaction.status-changed",
  "createdAt": "2026-06-01T12:01:00Z",
  "data": {
    "transaction": {
      "id": "d4e5f6a7-b8c9-4d0e-1f2a-b3c4d5e6f7a8",
      "status": "completed"
    }
  }
}

Via polling

GET /core/transactions/{transactionId} returns the current state. Use polling as a fallback when webhook delivery cannot be guaranteed. Terminal statuses are completed and failed.
GET /core/transactions/a0b1c2d3-e4f5-4a6b-7c8d-e9f0a1b2c3d4
{
  "transaction": {
    "id": "a0b1c2d3-e4f5-4a6b-7c8d-e9f0a1b2c3d4",
    "status": "completed"
  }
}
Prefer webhooks over polling in production. See Webhooks for setup instructions.

Notify the user

Display a confirmation when the final core.transaction.status-changed webhook fires with status: "completed". For card payouts, include the payout amount and currency so the user knows the card credit to expect. For bank withdrawals, confirm the amount and destination account once the withdrawal settles.
You now support crypto sell via the REST API.

Sample transaction

A sell to a card debits the user’s internal crypto account, converts at the locked quote rate, and pays out fiat to the linked card via OCT. The transaction fires a single core.transaction.status-changed webhook on completion.
{
  "transaction": {
    "id": "c9d0e1f2-a3b4-4c5d-6e7f-a8b9c0d1e2f3",
    "status": "completed",
    "origin": {
      "amount": "0.0049",
      "asset": "BTC",
      "rate": "0.00001",
      "node": {
        "type": "account",
        "id": "f8c2d1e4-3a7b-4c9d-8e5f-2b1a6d3c7e8f",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "destination": {
      "amount": "490.00",
      "asset": "USD",
      "rate": "1",
      "node": {
        "type": "external-account",
        "id": "aa6e6efa-8d73-497c-8278-0347f459bd68",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "USD",
      "amount": "490.00",
      "target": "destination",
      "rate": "0.00001"
    },
    "fees": [],
    "quotedAt": "2026-06-01T13:00:00Z",
    "createdAt": "2026-06-01T13:00:05Z",
    "updatedAt": "2026-06-01T13:00:30Z"
  }
}

Failure handling

FailureWhat happens to fundsAction
Quote expires before user confirmsQuote terminates; no debit from crypto accountRe-quote and present to user
Insufficient crypto balanceTransaction rejected at creation; no debitSurface error to user; they must top up
card-withdrawals capability missing or restrictedQuote creation succeeds but returns non-empty requirements; transaction creation blocked until resolvedPrompt user to complete the outstanding KYC step, then re-quote
Destination card fails octSupport checkTransaction fails before any debitRe-verify octSupport on the card external account; relink if needed
Card OCT declined by issuer at payoutCrypto is automatically credited back to the user’s internal Uphold accountcore.transaction.status-changed fires with status: "failed"; notify the user
Rate slippage — quote expired between quote and transactionTransaction creation returns a quote-expired error; no debitRe-quote and retry
Every terminal state — completed or failed — fires a core.transaction.status-changed webhook. Listen for all three to keep your UI in sync.
If the card OCT is declined after the crypto has been debited, the crypto is returned to the user’s internal account — not to an external wallet. Make sure your UI communicates this clearly.