Skip to main content

TL;DR

Swap any token on Solana in two API calls. Get a quote and unsigned transaction, sign it, submit it. No RPC node, no routing logic, no slippage math. Jupiter handles routing across multiple DEXs and 20+ market makers, MEV protection, and transaction landing. Base URL: https://api.jup.ag/ultra/v1

Prerequisites

  1. Get an API key at portal.jup.ag (free)
  2. All requests need the x-api-key header
  3. Install @solana/web3.js for transaction signing

Quick start

curl -X GET "https://api.jup.ag/ultra/v1/order?inputMint=So11111111111111111111111111111111111111112&outputMint=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&amount=100000000&taker=YOUR_WALLET_ADDRESS" \
  -H "x-api-key: YOUR_API_KEY"
You get back a transaction (base64 unsigned) and a requestId. Sign the transaction, then hit execute:
curl -X POST "https://api.jup.ag/ultra/v1/execute" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "signedTransaction": "BASE64_SIGNED_TX",
    "requestId": "REQUEST_ID_FROM_ORDER"
  }'
Done. Your swap lands in 50-400ms via Jupiter Beam.

API reference

Base URL: https://api.jup.ag/ultra/v1
EndpointDescription
GET /orderGet a swap quote and unsigned transaction
POST /executeSubmit signed transaction for execution
GET /holdings/{address}Get wallet token balances
GET /shield?mints={mints}Get token security warnings
GET /search?query={query}Search tokens by name, symbol, or mint
Full schema at API Reference.

The swap flow

Three steps: get order, sign, execute.

Step 1: Get an order

GET /order takes the token pair, amount, and the taker’s wallet. It returns a quote with an unsigned transaction ready to sign.
curl -X GET "https://api.jup.ag/ultra/v1/order?inputMint=So11111111111111111111111111111111111111112&outputMint=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&amount=100000000&taker=YOUR_WALLET_ADDRESS" \
  -H "x-api-key: YOUR_API_KEY"
Parameters:
ParameterRequiredDescription
inputMintYesInput token mint address
outputMintYesOutput token mint address
amountYesAmount in native token units (before decimals)
takerNoUser’s wallet address. Required to get a signable transaction. Omit to get a quote only (useful for showing price before wallet connect).
referralAccountNoReferral account for integrator fees
referralFeeNoReferral fee in basis points (50-255)
excludeRoutersNoManual mode only. Comma-separated routers to exclude: iris, jupiterz, dflow, okx
excludeDexesNoManual mode only. Comma-separated DEXs to exclude (applies to Iris router only). Full list of DEXs.
What you get back:
FieldWhat it means
transactionBase64 unsigned transaction. Sign this. null if you didn’t pass taker.
requestIdPass this to /execute. It’s how Ultra tracks your order.
outAmountExpected output in native token units
routerWhich router won the auction: iris, jupiterz, dflow, okx
gaslesstrue if this swap is gasless
slippageBpsSlippage tolerance set by RTSE (you don’t need to configure this)
feeBpsTotal fee in basis points
feeMintWhich token the fee is taken in
priceImpactPrice impact as a percentage
errorCodePresent when simulation fails (1 = insufficient funds, 2 = need SOL for gas, 3 = below gasless minimum)

Step 2: Sign the transaction

Deserialise the base64 transaction, sign it, serialise it back:
import { VersionedTransaction, Keypair } from '@solana/web3.js';
import bs58 from 'bs58';

// Load wallet (use env vars in production, not hardcoded keys)
const wallet = Keypair.fromSecretKey(bs58.decode(process.env.PRIVATE_KEY));

// Deserialise and sign
const transaction = VersionedTransaction.deserialize(
  Buffer.from(orderResponse.transaction, 'base64')
);
transaction.sign([wallet]);

const signedTransaction = Buffer.from(transaction.serialize()).toString('base64');
Don’t modify the transaction. Ultra transactions are meant to be signed and submitted as-is.

Step 3: Execute

Post the signed transaction and requestId to /execute. Jupiter broadcasts it via Beam and polls for the result. You get back the final status.
curl -X POST "https://api.jup.ag/ultra/v1/execute" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "signedTransaction": "BASE64_SIGNED_TX",
    "requestId": "REQUEST_ID_FROM_ORDER"
  }'
Connection dropped? Re-submit the same signedTransaction and requestId to /execute for up to 2 minutes. The transaction won’t double-execute because it has the same signature. This is your built-in polling mechanism.

Full working example

End-to-end: swap 1 SOL to USDC, from scratch.
import { VersionedTransaction, Keypair } from '@solana/web3.js';
import bs58 from 'bs58';

const API_KEY = 'YOUR_API_KEY';
const wallet = Keypair.fromSecretKey(bs58.decode(process.env.PRIVATE_KEY));

// 1. Get order
const orderResponse = await (
  await fetch(
    'https://api.jup.ag/ultra/v1/order?' +
      'inputMint=So11111111111111111111111111111111111111112' +
      '&outputMint=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' +
      '&amount=1000000000' +
      `&taker=${wallet.publicKey.toBase58()}`,
    { headers: { 'x-api-key': API_KEY } }
  )
).json();

if (!orderResponse.transaction) {
  throw new Error(orderResponse.errorMessage || orderResponse.error || 'No transaction returned');
}

console.log(`Quote: ${orderResponse.inAmount}${orderResponse.outAmount}`);
console.log(`Router: ${orderResponse.router} | Gasless: ${orderResponse.gasless} | Fee: ${orderResponse.feeBps} bps`);

// 2. Sign
const transaction = VersionedTransaction.deserialize(
  Buffer.from(orderResponse.transaction, 'base64')
);
transaction.sign([wallet]);
const signedTransaction = Buffer.from(transaction.serialize()).toString('base64');

// 3. Execute
const executeResponse = await (
  await fetch('https://api.jup.ag/ultra/v1/execute', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': API_KEY,
    },
    body: JSON.stringify({
      signedTransaction,
      requestId: orderResponse.requestId,
    }),
  })
).json();

if (executeResponse.status === 'Success') {
  console.log(`Done: https://solscan.io/tx/${executeResponse.signature}`);
  console.log(`In: ${executeResponse.inputAmountResult} | Out: ${executeResponse.outputAmountResult}`);
} else {
  console.error(`Failed (${executeResponse.code}): ${executeResponse.error}`);
}
That’s the entire integration.

Response format

Order response

interface OrderResponse {
  mode: string;
  inputMint: string;
  outputMint: string;
  inAmount: string;                // Input amount in native units
  outAmount: string;               // Expected output in native units
  otherAmountThreshold: string;    // Minimum output after slippage
  swapMode: string;                // "ExactIn"
  slippageBps: number;             // Set automatically by RTSE
  routePlan: RoutePlan[];
  router: 'iris' | 'jupiterz' | 'dflow' | 'okx';
  transaction: string | null;      // Base64 unsigned tx (null if no taker)
  requestId: string;               // Pass to /execute
  gasless: boolean;
  feeBps: number;                  // Total fee in bps
  feeMint: string;                 // Token mint fees are collected in
  inUsdValue: number;
  outUsdValue: number;
  priceImpact: number;
  signatureFeeLamports: number;
  prioritizationFeeLamports: number;
  rentFeeLamports: number;
  totalTime: number;               // API response time in ms
  // JupiterZ only
  quoteId?: string;
  maker?: string;
  expireAt?: string;
  // Present when simulation fails but quote exists
  errorCode?: number;
  errorMessage?: string;
}

interface RoutePlan {
  swapInfo: {
    ammKey: string;
    label: string;       // e.g. "MeteoraDLMM", "JupiterZ", "Raydium"
    inputMint: string;
    outputMint: string;
    inAmount: string;
    outAmount: string;
  };
  percent: number;       // Route split percentage
  bps: number;           // Route split in basis points
}

Execute response

interface ExecuteResponse {
  status: 'Success' | 'Failed';
  signature: string;
  slot: string;
  code: number;                    // 0 = success, negative = error
  error?: string;
  inputAmountResult?: string;      // Actual input consumed
  outputAmountResult?: string;     // Actual output received
  swapEvents?: SwapEvent[];        // Breakdown of individual swaps in the route
}

interface SwapEvent {
  inputMint: string;
  inputAmount: string;
  outputMint: string;
  outputAmount: string;
}

Error handling

Order errors

The order can return a quote but fail simulation. When that happens, you’ll see errorCode and errorMessage alongside the quote data, but transaction will be empty.
errorCodeWhat happenedWhat to show the user
1Insufficient funds”Not enough tokens to swap”
2Not enough SOL for gas”Top up X SOL for gas fees”
3Below gasless minimum”Minimum ~$10 swap for gasless”
Show the quote even when there’s an error. Users want to see the price. Just disable the swap button and explain why.

Execute errors

CodeWhat happenedWhat to do
0SuccessShow the Solscan link
-1Order not found or expiredGet a new order
-2Invalid signed transactionCheck your signing logic
-3Invalid message bytesDon’t modify the transaction
-4Missing request IDInclude requestId from the order response
-5Missing signed transactionInclude signedTransaction in the request body
-1000Failed to land on networkRetry with a fresh order
-1005Transaction expiredGet a new order and try again
-1006Timed outGet a new order and try again
6001Slippage exceededMarket moved. Retry with a new order.
For the full list of error codes including aggregator and RFQ-specific codes, see Response docs.

Gasless swaps

Ultra handles gasless automatically. You don’t configure anything. Ultra Gasless Support (Iris). When the taker has less than 0.01 SOL and the trade is at least ~$10, Jupiter covers everything: network fee, priority fee, token account rent, and other accounts rent (e.g. DEX-specific accounts). The cost gets deducted from the swap output (you’ll see it in feeBps). JupiterZ (RFQ). When a market maker provides the route, the market maker pays network and priority fees. But they don’t cover token account rent, so the user still needs enough SOL for that. Check gasless: true in the order response to know if it kicked in. For the full breakdown of scenarios, see Gasless Support.

Integrator fees

You can earn fees on swaps through your integration. Ultra handles collection.
  1. Create a referral account using the @jup-ag/referral-sdk or the Referral Dashboard
  2. Create referral token accounts for the mints you expect to collect fees in (at minimum: SOL, USDC, USDT)
  3. Pass referralAccount and referralFee to /order
const orderResponse = await (
  await fetch(
    'https://api.jup.ag/ultra/v1/order?' +
      'inputMint=So11111111111111111111111111111111111111112' +
      '&outputMint=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' +
      '&amount=100000000' +
      '&taker=YOUR_WALLET_ADDRESS' +
      '&referralAccount=YOUR_REFERRAL_ACCOUNT' +
      '&referralFee=50',  // 50 bps = 0.5%
    { headers: { 'x-api-key': 'YOUR_API_KEY' } }
  )
).json();

// Verify your fee was applied
console.log(`Fee: ${orderResponse.feeBps} bps in ${orderResponse.feeMint}`);
Fees range from 50 to 255 bps. Jupiter takes 20% of your integrator fee (no additional Ultra base fee on top). Ultra decides which token to collect fees in based on a priority list. If the referral token account for that mint isn’t initialised, the swap still goes through, but without your fee. So always check that feeBps matches what you set. Full setup walkthrough: Add Integrator Fees.

Fees

Ultra charges 0-10 bps on most swaps, 50 bps for new tokens. Lower than the industry average by 8-10x.
Token pairFee
SOL/Stable → JUP/JLP/jupSOL0 bps
Stable ↔ Stable, LST ↔ LST0 bps
SOL ↔ Stable2 bps
LST ↔ Stable5 bps
Everything else10 bps
New tokens (< 24h old)50 bps
Full breakdown: Fees.

Common questions

Nope. Ultra runs on Jupiter’s own validator stake and dedicated infra so you don’t have to. You just sign the transaction.
No. Ultra transactions must be submitted as-is. This is what lets Jupiter provide end-to-end observability, customer support, and continuous optimisation across your swaps.
/order averages ~300ms (P50). Transaction landing via Jupiter Beam is 50-400ms. End-to-end including polling, 95% of swaps complete under 2 seconds.
Dynamic. Base is 50 requests per 10-second sliding window, and it scales up with your swap volume. $1M in daily volume gets you ~165 req/10s. No paid tiers. Just get a free API key at portal.jup.ag. Details: Rate Limits.
Yes. Omit the taker parameter. You’ll get the full quote (pricing, route plan, fees) but no transaction field. Good for showing price previews before wallet connect.
Re-submit the same signedTransaction and requestId to /execute. You can do this for up to 2 minutes. The transaction has the same signature so it can’t double-execute. This is your built-in retry and polling mechanism.

Next steps

Built something? Tag @JupDevRel or @0xanmol.