Skip to main content
Jupiter Swap API V2: Build a Token Swap Interface - Featured image
DeFi

Jupiter Swap API V2: Build a Token Swap Interface

BUILDERZ

BUILDERZ

Share

In this guide

The decisions and working steps ahead.

    Swap API V2 in one minute

    What happened to the old endpoints

    Step 1: Fetch an order from a server route

A lot of Jupiter swap code still calls the V1 quote and swap endpoints. Jupiter's Swap API docs now say that API is no longer actively maintained.

Your swap UI may still run on it. This guide rebuilds the flow on Swap API V2 in three steps. You'll fetch an order, sign it, execute it, and handle the codes that decide whether a swap lands.

Save it as a checklist for your next swap integration.

Swap API V2 in one minute

Jupiter's Swap API V2 has one base URL. It offers two paths, and you pick one per integration.

Code exampletext
Base URL:   https://api.jup.ag/swap/v2

Path A (Meta-Aggregator)
  GET  /order     quote plus an assembled transaction
  POST /execute   Jupiter lands your signed transaction

Path B (Router)
  GET  /build     raw swap instructions, you land it yourself

Path A lets several routers compete for your quote. You get back a ready transaction, and Jupiter lands it for you. Swap fees apply.

Path B returns raw swap instructions from the Metis router only. You add your own instructions and send the transaction yourself. There's no Jupiter swap fee.

Jupiter recommends starting with Path A. We'll build that first, because it covers a plain wallet-to-wallet swap with the least code.

What happened to the old endpoints

The Metis Swap API page says V1 has been replaced by Swap V2.

If you used the V1 quote and swap calls, you move to order and execute. If you used the swap instructions call, you move to build.

Ultra API users also move to order and execute. So there's now one swap API to learn instead of two.

Step 1: Fetch an order from a server route

Requests without a key work, but they're capped at 0.5 requests per second.

That limit comes from the rate limit section of the V2 docs.

For production, you send a key in an API key header.

Keys come from the Jupiter Portal. One key covers every Jupiter API.

Each paid tier raises the request limit on the main endpoints. The execute call has its own bucket that doesn't count against it. Either way, debounce price checks while the user types, so a fast typist doesn't burn your quota.

That header is your first design decision. If the browser calls Jupiter directly, your key ships in the bundle. Put a thin server route in front of it instead.

Here's a Next.js route handler that forwards the order call:

Code exampletypescript
// app/api/swap/order/route.ts
const JUP = "https://api.jup.ag/swap/v2";

export async function GET(req: Request) {
  const q = new URL(req.url).searchParams;
  const params = new URLSearchParams({
    inputMint: q.get("inputMint") ?? "",
    outputMint: q.get("outputMint") ?? "",
    amount: q.get("amount") ?? "", // smallest unit, as a string
    taker: q.get("taker") ?? "", // the user's wallet address
  });

  const res = await fetch(`${JUP}/order?${params}`, {
    headers: { "x-api-key": process.env.JUPITER_API_KEY! },
    cache: "no-store", // quotes go stale fast
  });

  return new Response(await res.text(), { status: res.status });
}

The taker field matters more than it looks. Without a taker wallet, the order docs say you get a quote but no transaction.

Use that to your advantage. Leave the taker out while the user is still typing an amount, so you can show a price. Add it only when they press the swap button.

Convert amounts without floats

The amount field takes the token's smallest unit. Pass 1 for SOL and you're asking to swap one lamport.

Floats make this worse, because a value like 0.1 can't be stored exactly. Do the conversion on strings instead:

Code exampletypescript
export function toBaseUnits(value: string, decimals: number): string {
  const [whole, frac = ""] = value.trim().split(".");
  const padded = (frac + "0".repeat(decimals)).slice(0, decimals);
  return BigInt(whole + padded).toString();
}

// toBaseUnits("0.1", 9) === "100000000"

Read the decimals from the mint account, not from a table you typed by hand. A wrong decimal count turns into a wrong trade size, and the swap will still succeed.

Step 2: Sign and execute the swap

The order response holds a base64 transaction and a request ID. Your interface decodes the transaction and asks the wallet to sign it.

Here's the client side, built on Solana Wallet Adapter:

Code exampletypescript
import { VersionedTransaction } from "@solana/web3.js";
import { useWallet } from "@solana/wallet-adapter-react";

export function useJupiterSwap() {
  const { publicKey, signTransaction } = useWallet();

  return async function swap(inputMint: string, outputMint: string, amount: string) {
    if (!publicKey || !signTransaction) throw new Error("Connect a wallet first");

    const q = new URLSearchParams({ inputMint, outputMint, amount, taker: publicKey.toBase58() });
    const order = await (await fetch(`/api/swap/order?${q}`)).json();
    if (!order.transaction) {
      throw new Error(`No route: ${order.router} ${order.errorCode}`);
    }

    const tx = VersionedTransaction.deserialize(Buffer.from(order.transaction, "base64"));
    const signed = await signTransaction(tx);

    const res = await fetch("/api/swap/execute", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        signedTransaction: Buffer.from(signed.serialize()).toString("base64"),
        requestId: order.requestId,
      }),
    });
    return res.json(); // { status, signature, code, ... }
  };
}

Two signing details

Some routes need a second signature. For RFQ quotes from JupiterZ, the market maker signs during the execute call. The transaction you sign is partial by design, and your wallet only adds the user's signature.

The other detail is the transaction version. The order call returns a version 0 transaction by default, which web3.js v1 can sign.

Version 1 transactions are different. The order docs say they need Solana Kit v8 or later, and not every wallet supports them yet.

We used the same Next.js and Wallet Adapter stack when we built the sharky.fi royalty tool. It was a white-label royalty app for the sharky community.

Forward the signed transaction

The execute route mirrors the order route. It forwards the signed transaction and the request ID, and adds your key on the way out.

Code exampletypescript
// app/api/swap/execute/route.ts
export async function POST(req: Request) {
  const { signedTransaction, requestId } = await req.json();

  const res = await fetch("https://api.jup.ag/swap/v2/execute", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": process.env.JUPITER_API_KEY!,
    },
    body: JSON.stringify({ signedTransaction, requestId }),
  });

  const result = await res.json();
  if (result.code !== 0) console.error("swap failed", result.code, requestId);
  return Response.json(result, { status: res.status });
}

Both fields are required, per the execute docs. The response carries a status, a signature, and a numeric code.

Log the code with the request ID on the server. When a user says a swap failed, that pair is how you find out why.

Step 3: Handle the codes that cost you swaps

A swap can fail in three places: the order, the signature, or the landing. Your interface should say which one happened.

Match on codes, not messages

When the order call can't build a transaction, it returns an empty one with an error code and a message. Jupiter's guidance is to match on the router name plus the error code. The message text can change.

The execute call returns code 0 on success. The error code list groups the rest like this:

  • -1: the cached order is missing. The request ID wasn't found or has expired, so fetch a new order.
  • -2 and -3: the signed transaction or its message bytes are invalid. Check that nothing changed the transaction after you decoded it.
  • -1000 to -1005: aggregator failures, such as failed to land or expired before landing.
  • -2000 to -2004: RFQ failures, such as quote expired or swap rejected.

Map these to three user messages: try again, the price moved, or check your wallet. Keep the raw code in your logs.

Never reuse an old order

Quotes go stale as prices move. The docs advise signing and sending as soon as you get the order.

Aggregator routes expire at the last valid block height. RFQ routes expire at the market maker's own deadline, which is usually sooner.

In UI terms, fetch the order with a taker only after the user clicks swap. If the wallet popup sits open for a minute, expect an expiry code. Catch it, fetch a fresh order, and ask the user to confirm the new price.

When to use build instead of order

The order path covers a plain swap. Switch to build when the swap is one step inside a larger transaction.

Typical cases:

  • You add your own instructions before or after the swap, like a deposit into your program.
  • Your program calls Jupiter through a cross-program invocation.
  • You already run your own landing pipeline with priority fees and retries.

The build docs list four required inputs: the two mints, the amount, and the taker.

The response returns compute budget, setup, swap, and cleanup instructions, plus lookup table addresses. You assemble the transaction yourself.

Slippage defaults to 50 basis points (0.5%) on build, per the same page. There's one hard rule: a build transaction can't go through execute, because it has no request ID. You send it through your own RPC, or through Jupiter's submit endpoint with a SOL tip.

If you're building a DeFi product rather than a swap widget, this is usually your path. Our Solana DeFi development work starts with that choice. It decides who owns transaction landing when things go wrong.

FAQ

Is the Jupiter V1 quote endpoint deprecated?

Jupiter says the Metis Swap API V1 is no longer actively maintained and has been replaced by Swap V2. New builds should use order and execute, or build for custom transactions.

The V2 docs map each old endpoint to its replacement.

Do I need an API key for the Jupiter Swap API?

No. Keyless requests work, but they're limited to 0.5 requests per second. Production apps should send a key from the Jupiter Portal, and one key covers all Jupiter APIs.

Should I call Jupiter from the browser or the server?

Call it from the server whenever you use an API key. A browser call exposes the key in your bundle. A small server route that forwards order and execute keeps it private and gives you one place to log errors.

Why does my Jupiter swap fail with code -1?

Code -1 means the cached order is missing, because the request ID wasn't found or has expired. It often happens when the user waits too long in the wallet popup. Fetch a new order and ask the user to confirm the new price.

What's the difference between Jupiter order and build?

Order returns a ready transaction from several competing routers, and Jupiter lands it through execute. Build returns raw instructions from Metis only, with no Jupiter swap fee. Use build when you need custom instructions, CPI, or your own landing pipeline.

Ship the swap, then the product

A swap call is a small part of a DeFi product. The harder parts are custody, failure handling, and who answers when a transaction doesn't land.

Before you ship, run the flow once per failure path. Let a quote expire in the wallet popup, reject a signature, and swap a token with unusual decimals. If your interface tells the user what happened each time, you're ready.

Building a swap flow into a Solana product? Send us a brief and tell us which path you picked.

Continue reading

Want to turn your idea into reality?

Book a 20-minute intro call, or send a short project brief. We reply within one business day.