Skip to main content
A buy moves value into the user’s Uphold crypto account, funded from a fiat external account (card or bank).

Prerequisites

  • The user has completed onboarding.
  • Capabilities — card deposits require the cards capability and the fiat → crypto leg requires trades (UK retail users have a 24-hour post-onboarding cooldown before trading). Bank deposit 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 buy walkthrough.

Select a crypto to buy

Call List assets to retrieve the assets available on the platform. Filter to assets that include buy in their features array to show only purchasable assets.
GET /core/assets
{
  "assets": [
    {
      "code": "BTC",
      "name": "Bitcoin",
      "type": "crypto",
      "symbol": "₿",
      "decimals": 8,
      "features": [
        "buy",
        "deposit",
        "sell",
        "transfer",
        "withdraw"
      ]
    },
    {
      "code": "ETH",
      "name": "Ethereum",
      "type": "crypto",
      "symbol": "Ξ",
      "decimals": 18,
      "features": [
        "buy",
        "deposit",
        "sell",
        "transfer",
        "withdraw"
      ]
    }
  ]
}
The chosen asset (BTC in this example) becomes the destination.asset for the buy. Call List accounts to find the user’s existing Uphold accounts for that asset.
GET /core/accounts
If the user does not yet have an account for the chosen asset, create one with Create account.
POST /core/accounts
{
  "label": "My BTC Account",
  "asset": "BTC"
}

Define the source

The source determines how the buy is funded. Choose the path that matches the user’s intent:

Fiat external account

The user funds the buy from a linked card or bank account.

Check available rails

Call List Rails to verify the deposit rail you plan to use is available. 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.
Bank: For bank accounts, the linking flow varies by rail. See the per-rail guides: Once the user has a linked bank external account, retrieve it with List external accounts.
GET /core/external-accounts
{
  "externalAccounts": [
    {
      "id": "c4a2e8b1-5d6f-4a3b-9e7c-8f1d2a3b4c5d",
      "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"
    }
  ]
}

Transaction preview

Show the user a preview of the buy 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

A fiat buy runs in two sequenced legs: a fiat deposit into the user’s Uphold fiat account, followed by a fiat-to-crypto trade. Create a quote for each leg. Deposit quote — card:
POST /core/transactions/quote
{
  "origin": {
    "type": "external-account",
    "id": "aa6e6efa-8d73-497c-8278-0347f459bd68"
  },
  "destination": {
    "type": "account",
    "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8"
  },
  "denomination": {
    "asset": "USD",
    "amount": "300.00",
    "target": "origin"
  }
}
{
  "quote": {
    "id": "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5",
    "origin": {
      "amount": "300.00",
      "asset": "USD",
      "rate": "1",
      "node": {
        "type": "external-account",
        "id": "aa6e6efa-8d73-497c-8278-0347f459bd68",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "destination": {
      "amount": "300.00",
      "asset": "USD",
      "rate": "1",
      "node": {
        "type": "account",
        "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "USD",
      "amount": "300.00",
      "target": "origin",
      "rate": "1"
    },
    "fees": [],
    "expiresAt": "2026-06-01T12:00:30Z"
  }
}
Deposit quote — bank (example: SEPA):
POST /core/transactions/quote
{
  "origin": {
    "type": "external-account",
    "id": "b7f3c2d9-4e1a-4b8f-9c2e-5d6a7b8c9d0e"
  },
  "destination": {
    "type": "account",
    "id": "c11608ff-739d-4f38-bf91-f2d51c3b9c19"
  },
  "denomination": {
    "asset": "EUR",
    "amount": "300.00",
    "target": "origin"
  }
}
{
  "quote": {
    "id": "b2c3d4e5-f6a7-4b8c-9d0e-f1a2b3c4d5e6",
    "origin": {
      "amount": "300.00",
      "asset": "EUR",
      "rate": "1",
      "node": {
        "type": "external-account",
        "id": "b7f3c2d9-4e1a-4b8f-9c2e-5d6a7b8c9d0e",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "destination": {
      "amount": "300.00",
      "asset": "EUR",
      "rate": "1",
      "node": {
        "type": "account",
        "id": "c11608ff-739d-4f38-bf91-f2d51c3b9c19",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "EUR",
      "amount": "300.00",
      "target": "origin",
      "rate": "1"
    },
    "fees": [],
    "expiresAt": "2026-06-01T12:00:30Z"
  }
}
Trade quote (fiat → crypto): After presenting the deposit quote, also create the trade quote so the user can see the full buy preview — the deposit amount and the crypto amount they will receive — before confirming.
For bank deposits, this trade quote is for preview only. Bank deposits settle asynchronously (hours to days), so this quote will have expired long before the deposit lands. Re-quote at the time you confirm the trade leg, after the deposit status: "completed" webhook fires.
POST /core/transactions/quote
{
  "origin": {
    "type": "account",
    "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8"
  },
  "destination": {
    "type": "account",
    "id": "f8c2d1e4-3a7b-4c9d-8e5f-2b1a6d3c7e8f"
  },
  "denomination": {
    "asset": "USD",
    "amount": "300.00",
    "target": "origin"
  }
}
{
  "quote": {
    "id": "c3d4e5f6-a7b8-4c9d-0e1f-a2b3c4d5e6f7",
    "origin": {
      "amount": "300.00",
      "asset": "USD",
      "rate": "1",
      "node": {
        "type": "account",
        "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "destination": {
      "amount": "0.003",
      "asset": "BTC",
      "rate": "0.00001",
      "node": {
        "type": "account",
        "id": "f8c2d1e4-3a7b-4c9d-8e5f-2b1a6d3c7e8f",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "USD",
      "amount": "300.00",
      "target": "origin",
      "rate": "0.00001"
    },
    "fees": [],
    "expiresAt": "2026-06-01T12:01:30Z"
  }
}

Confirm quote

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

Fiat external account

Confirm the deposit leg first, then the trade leg after the deposit settles. Deposit — card (requires 3DS): Always include a returnUrl — the card issuer may require 3DS authorization. You may embed stateful data in the returnUrl query parameters to resume the flow after redirect. Include X-Uphold-User-Ip and X-Uphold-User-Agent headers — they are used by the platform for 3DS risk evaluation.
POST /core/transactions
X-Uphold-User-Ip: <user-ip>
X-Uphold-User-Agent: <user-agent>
{
  "quoteId": "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5",
  "params": {
    "returnUrl": "https://example.com/return"
  }
}
{
  "transaction": {
    "id": "d4e5f6a7-b8c9-4d0e-1f2a-b3c4d5e6f7a8",
    "status": "processing",
    "origin": {
      "amount": "300.00",
      "asset": "USD",
      "rate": "1",
      "node": {
        "type": "external-account",
        "id": "aa6e6efa-8d73-497c-8278-0347f459bd68",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604",
        "confirmationUrl": "https://authentication-devices.sandbox.checkout.com/sessions-interceptor/sid_zrih63pnk6ietkpuerqiyd5zga"
      }
    },
    "destination": {
      "amount": "300.00",
      "asset": "USD",
      "rate": "1",
      "node": {
        "type": "account",
        "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "USD",
      "amount": "300.00",
      "target": "origin",
      "rate": "1"
    },
    "fees": [],
    "quotedAt": "2026-06-01T12:00:00Z",
    "createdAt": "2026-06-01T12:00:05Z",
    "updatedAt": "2026-06-01T12:00:05Z"
  }
}
If origin.node.confirmationUrl is present, redirect the user to that URL to complete the 3DS challenge. After completion, the issuer redirects the user back to your returnUrl. Wait for the core.transaction.status-changed webhook with status: "completed" before proceeding to the trade leg. Deposit — bank: Bank deposits do not require a returnUrl or 3DS.
POST /core/transactions
{
  "quoteId": "b2c3d4e5-f6a7-4b8c-9d0e-f1a2b3c4d5e6"
}
{
  "transaction": {
    "id": "e5f6a7b8-c9d0-4e1f-2a3b-c4d5e6f7a8b9",
    "status": "processing",
    "origin": {
      "amount": "300.00",
      "asset": "EUR",
      "rate": "1",
      "node": {
        "type": "external-account",
        "id": "b7f3c2d9-4e1a-4b8f-9c2e-5d6a7b8c9d0e",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "destination": {
      "amount": "300.00",
      "asset": "EUR",
      "rate": "1",
      "node": {
        "type": "account",
        "id": "c11608ff-739d-4f38-bf91-f2d51c3b9c19",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "EUR",
      "amount": "300.00",
      "target": "origin",
      "rate": "1"
    },
    "fees": [],
    "quotedAt": "2026-06-01T12:00:00Z",
    "createdAt": "2026-06-01T12:00:05Z",
    "updatedAt": "2026-06-01T12:00:05Z"
  }
}
Bank deposits settle asynchronously — instantly for FPS / FedNow, hours to days for ACH / SEPA / wire. See the per-rail bank-transfer deposit guides for timing details.
Wait for the core.transaction.status-changed webhook with status: "completed" before proceeding to the trade leg. Trade (fiat → crypto): Once the deposit settles, confirm the trade quote.
POST /core/transactions
{
  "quoteId": "c3d4e5f6-a7b8-4c9d-0e1f-a2b3c4d5e6f7"
}
{
  "transaction": {
    "id": "f6a7b8c9-d0e1-4f2a-3b4c-d5e6f7a8b9c0",
    "status": "processing",
    "origin": {
      "amount": "300.00",
      "asset": "USD",
      "rate": "1",
      "node": {
        "type": "account",
        "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "destination": {
      "amount": "0.003",
      "asset": "BTC",
      "rate": "0.00001",
      "node": {
        "type": "account",
        "id": "f8c2d1e4-3a7b-4c9d-8e5f-2b1a6d3c7e8f",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "USD",
      "amount": "300.00",
      "target": "origin",
      "rate": "0.00001"
    },
    "fees": [],
    "quotedAt": "2026-06-01T12:01:00Z",
    "createdAt": "2026-06-01T12:01:05Z",
    "updatedAt": "2026-06-01T12:01:05Z"
  }
}
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 an in-app confirmation when each core.transaction.status-changed webhook fires with status: "completed". Show a final confirmation once the last leg settles — the crypto is now credited to the user’s Uphold account.
You now support crypto buy via the REST API.

Sample transactions

A buy via a fiat external account produces two chained transactions — a fiat deposit and a fiat-to-crypto trade. Each follows the same lifecycle and fires its own core.transaction.status-changed webhook on completion.

Card deposit

{
  "transaction": {
    "id": "d4e5f6a7-b8c9-4d0e-1f2a-b3c4d5e6f7a8",
    "status": "completed",
    "origin": {
      "amount": "300.00",
      "asset": "USD",
      "rate": "1",
      "node": {
        "type": "external-account",
        "id": "aa6e6efa-8d73-497c-8278-0347f459bd68",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "destination": {
      "amount": "300.00",
      "asset": "USD",
      "rate": "1",
      "node": {
        "type": "account",
        "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "USD",
      "amount": "300.00",
      "target": "origin",
      "rate": "1"
    },
    "fees": [],
    "quotedAt": "2026-06-01T12:00:00Z",
    "createdAt": "2026-06-01T12:00:05Z",
    "updatedAt": "2026-06-01T12:01:00Z"
  }
}

Bank deposit (SEPA)

{
  "transaction": {
    "id": "e5f6a7b8-c9d0-4e1f-2a3b-c4d5e6f7a8b9",
    "status": "completed",
    "origin": {
      "amount": "300.00",
      "asset": "EUR",
      "rate": "1",
      "node": {
        "type": "external-account",
        "id": "b7f3c2d9-4e1a-4b8f-9c2e-5d6a7b8c9d0e",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "destination": {
      "amount": "300.00",
      "asset": "EUR",
      "rate": "1",
      "node": {
        "type": "account",
        "id": "c11608ff-739d-4f38-bf91-f2d51c3b9c19",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "EUR",
      "amount": "300.00",
      "target": "origin",
      "rate": "1"
    },
    "fees": [],
    "quotedAt": "2026-06-01T12:00:00Z",
    "createdAt": "2026-06-01T12:00:05Z",
    "updatedAt": "2026-06-01T12:01:00Z"
  }
}

Fiat-to-crypto trade

{
  "transaction": {
    "id": "f6a7b8c9-d0e1-4f2a-3b4c-d5e6f7a8b9c0",
    "status": "completed",
    "origin": {
      "amount": "300.00",
      "asset": "USD",
      "rate": "1",
      "node": {
        "type": "account",
        "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "destination": {
      "amount": "0.003",
      "asset": "BTC",
      "rate": "0.00001",
      "node": {
        "type": "account",
        "id": "f8c2d1e4-3a7b-4c9d-8e5f-2b1a6d3c7e8f",
        "ownerId": "cd21b26d-35d2-408a-9201-b8fdbef7a604"
      }
    },
    "denomination": {
      "asset": "USD",
      "amount": "300.00",
      "target": "origin",
      "rate": "0.00001"
    },
    "fees": [],
    "quotedAt": "2026-06-01T12:01:00Z",
    "createdAt": "2026-06-01T12:01:05Z",
    "updatedAt": "2026-06-01T12:01:30Z"
  }
}

Failure handling

LegStepFailureFunds
Deposit (card)QuoteCard declined or 3DS failed (charge never captured)Nothing moved; no fees charged
Deposit (card)TransactionQuote expired before commitNo transaction created; create a new quote
Deposit (card)TransactionCard captured but later reversed by issuerFiat balance reduced by reversal; Uphold absorbs processing fees
Deposit (card)Transactioncards capability not enabledNo transaction; prompt KYC
Deposit (bank)TransactionInsufficient funds or mandate not authorisedNo deposit received; surface error to user
Deposit (bank)TransactionSettlement timeout (rail-dependent)No deposit received; retry or contact support
If the deposit leg completes but the fiat → crypto conversion fails, the user’s fiat funds remain in their Uphold fiat account. Detect this via the core.transaction.status-changed webhook and either retry the conversion or surface the fiat balance to the user.
The core.transaction.status-changed webhook fires with status: "failed" for each failed transaction.