> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coinvoyage.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate from v2 to v3

> Upgrade a CoinVoyage v2 integration to the v3 API and PayKit SDK surface.

export const agentMigrationPrompt = ["# CoinVoyage v2 to v3 migration for coding agents", "", "You are migrating this project from CoinVoyage API v2 to v3.", "", "Use the CoinVoyage docs at https://docs.coinvoyage.io, especially https://docs.coinvoyage.io/resources/migration-v2-to-v3, as the source of truth. First inspect the codebase. Then update only CoinVoyage integration code. Preserve business logic, UI behavior, secrets handling, styling, and unrelated APIs.", "", "## Required steps", "", "1. Update CoinVoyage dependencies to v3 first, including @coin-voyage/paykit@^3.", "2. Find all CoinVoyage SDK imports, ApiClient setup, direct /v2 URLs, PayOrder types, payId props, swapData calls, webhook parsers, WebSocket event handlers, and payment/order calls.", "3. Replace v2 ApiClient initialization with ApiClient({ apiKey, environment }) from @coin-voyage/paykit/server.", "4. Replace PayOrder terminology and helper names with v3 Order terminology.", "5. Replace PayButton payId usage with orderId for server-created orders.", "6. Replace onPaymentStarted with the v3 lifecycle callbacks used by this project.", "7. Replace standalone swapData execution calls with swapExecute.", "8. Update direct HTTP calls from /v2 route families to the matching /v3 route families.", "9. Update webhook and WebSocket parsers to read the v3 envelope: { event, delivered_at, order }.", "10. Run the project's typecheck, lint, and tests. If a command is unavailable, report that clearly.", "11. Report changed files, remaining TODOs, and any ambiguous mappings.", "", "Do not guess missing business rules. If a field mapping is unclear, leave a TODO with the exact file and explain the ambiguity.", "", "## Search targets", "", "- @coin-voyage/paykit", "- ApiClient", "- PayOrder", "- PayOrderMode", "- PayOrderStatus", "- PayOrderParams", "- PayOrderIntent", "- createPayOrder", "- createDepositPayOrder", "- createSalePayOrder", "- createRefundPayOrder", "- getPayOrder", "- listPayOrders", "- payOrderQuote", "- payOrderPaymentDetails", "- getPayOrderPaymentMethods", "- payId", "- onPaymentStarted", "- swapData", "- payorder_", "- payorder_id", "- /v2/", "- /pay-orders", "- /quotes", "- /kyc", "- /withdrawals", "", "## Required replacements", "", "| Find | Replace with |", "|---|---|", "| PayOrder domain objects that directly represent CoinVoyage responses | Order |", "| payId passed to PayKit UI for a server-created payment | orderId |", "| createDepositPayOrder | createDepositOrder |", "| createSalePayOrder | createSaleOrder |", "| createRefundPayOrder | createRefundOrder |", "| getPayOrder | getOrder |", "| listPayOrders | listOrders |", "| payOrderQuote | orderQuotes |", "| payOrderPaymentDetails | createPayment |", "| getPayOrderPaymentMethods | getOrderPaymentMethods |", "| swapData | swapExecute |", "| webhook type with payorder_* values | webhook event with ORDER_* values |", "| webhook payorder_id | order.id |", "", "## Do not change", "", "- Do not rename unrelated app domain models just because they contain the word order.", "- Do not remove fulfillment, reconciliation, retry, or idempotency logic around completed payments.", "- Do not move API secrets into browser code. Keep signed and secret-backed calls server-side.", "- Do not hardcode production URLs if the SDK environment option already covers the target environment.", "- Do not delete v2 fallback code unless the project owner explicitly asks for a hard cutover.", "", "## Acceptance checks", "", "- No direct CoinVoyage /v2 calls remain unless they are intentionally kept behind a named fallback path.", "- Server code uses ApiClient({ apiKey, environment }) from @coin-voyage/paykit/server.", "- PayKit UI receives orderId for server-created orders.", "- Webhook handlers verify signatures before parsing and read { event, delivered_at, order }.", "- Fulfillment remains idempotent by the CoinVoyage order ID and the project's internal order or invoice ID.", "- Typecheck, lint, and relevant tests pass."].join("\n");

export const AgentMigrationCopy = () => {
  const [copied, setCopied] = useState(false);
  const [failed, setFailed] = useState(false);
  const copy = async () => {
    setFailed(false);
    try {
      await navigator.clipboard.writeText(agentMigrationPrompt);
      setCopied(true);
      window.setTimeout(() => setCopied(false), 2000);
      return;
    } catch (error) {
      try {
        const textarea = document.createElement("textarea");
        textarea.value = agentMigrationPrompt;
        textarea.setAttribute("readonly", "");
        textarea.style.position = "fixed";
        textarea.style.left = "-9999px";
        document.body.appendChild(textarea);
        textarea.select();
        document.execCommand("copy");
        document.body.removeChild(textarea);
        setCopied(true);
        window.setTimeout(() => setCopied(false), 2000);
      } catch (fallbackError) {
        setFailed(true);
      }
    }
  };
  return <div className="not-prose my-6 rounded-lg border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-900/40">
      <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
        <div className="flex flex-col">
          <p className="m-0 text-sm font-semibold text-zinc-950 dark:text-white">
            Migrating with an AI agent?
          </p>
          <p className="m-0 mt-1 text-sm text-zinc-600 dark:text-zinc-400">
            Copy a focused prompt that points to docs.coinvoyage.io, upgrades dependencies to v3 first, and lists the required code changes.
          </p>
        </div>
        <button type="button" onClick={copy} className="inline-flex shrink-0 items-center justify-center rounded-md bg-zinc-950 px-3 py-2 text-sm font-medium text-white transition hover:bg-zinc-800 dark:bg-white dark:text-zinc-950 dark:hover:bg-zinc-200">
          Copy for LLM
        </button>
      </div>
      {copied && <p className="m-0 mt-3 text-sm text-green-600 dark:text-green-400">Copied.</p>}
      {failed && <p className="m-0 mt-3 text-sm text-red-600 dark:text-red-400">Copy failed. Use the page-level Copy for LLM action instead.</p>}
    </div>;
};

CoinVoyage v3 is a breaking API and SDK update. The main change is the move from **PayOrder** terminology and helper names to **Order** terminology across the REST API, PayKit, webhooks, and WebSocket events.

<Info>
  v3 upgrade is breaking. Keep your v2 integration running until you have tested order creation, payment completion, webhooks, refunds, swaps, and any off-ramp workflows in your target environment.
</Info>

<AgentMigrationCopy />

## What changes

| Area                               | v2                                        | v3                                                               |
| ---------------------------------- | ----------------------------------------- | ---------------------------------------------------------------- |
| API base URL                       | `https://api.coinvoyage.io/v2`            | `https://api.coinvoyage.io/v3`                                   |
| Primary resource name              | PayOrder                                  | Order                                                            |
| Server-created PayButton prop      | `payId`                                   | `orderId`                                                        |
| PayButton lifecycle start callback | `onPaymentStarted`                        | `onAwaitingPayment`, `onConfirmingPayment`, `onExecutingPayment` |
| Webhook payload event field        | `type` with lowercase `payorder_*` values | `event` with uppercase `ORDER_*` values                          |
| Webhook payload resource ID        | `payorder_id`                             | `order.id`                                                       |

## Migration checklist

1. Upgrade CoinVoyage dependencies to v3, including `@coin-voyage/paykit@^3`.
2. Update `ApiClient` PayOrder helper names with the v3 Order helper names.
3. Update `PayButton` and `PayButton.Custom` from `payId` to `orderId`.
4. Replace `onPaymentStarted` with the more specific payment lifecycle callbacks.
5. Update webhook handlers to read `{ event, delivered_at, order }`.
6. Run a full test payment and verify that webhook fulfillment remains idempotent.

## Upgrade PayKit

```bash theme={null}
pnpm add @coin-voyage/paykit@^3
```

Use the package manager your project already uses.

## Update ApiClient initialization

`ApiClient` is now called as a factory function from `@coin-voyage/paykit/server`.

```typescript theme={null}
import { ApiClient } from "@coin-voyage/paykit/server";

const apiClient = ApiClient({
  apiKey: process.env.COIN_VOYAGE_API_KEY!,
  environment: "production",
});
```

When you manually sign HTTP requests, sign the path without the `/v3` prefix:

```typescript theme={null}
const authorization = apiClient.generateAuthorizationSignature(
  process.env.COIN_VOYAGE_API_SECRET!,
  "POST",
  "/orders"
);
```

## Rename order methods

Replace v2 PayOrder helper names with v3 Order helper names.

| v2 method                                          | v3 method                                       |
| -------------------------------------------------- | ----------------------------------------------- |
| `createDepositPayOrder(params)`                    | `createDepositOrder(params)`                    |
| `createSalePayOrder(params, apiSecret)`            | `createSaleOrder(params, apiSecret)`            |
| `createRefundPayOrder(orderId, params, apiSecret)` | `createRefundOrder(orderId, params, apiSecret)` |
| `getPayOrder(payOrderId)`                          | `getOrder(orderId)`                             |
| `listPayOrders(params, apiSecret)`                 | `listOrders(params, apiSecret)`                 |
| `payOrderQuote(orderId, params)`                   | `orderQuotes(orderId, params)`                  |
| `payOrderPaymentDetails(params)`                   | `createPayment(orderId, params)`                |
| `getPayOrderPaymentMethods(orderId)`               | `getOrderPaymentMethods(orderId)`               |

### Deposit order before and after

<CodeGroup>
  ```typescript v2 theme={null}
  const { data, error } = await apiClient.createDepositPayOrder({
    intent: {
      asset: {
        chain_id: ChainId.SUI,
        address: null,
      },
      amount: {
        token_amount: 10,
      },
      receiving_address: "0xDestinationWallet",
    },
  });
  ```

  ```typescript v3 theme={null}
  const { data, error } = await apiClient.createDepositOrder({
    amount: "10",
    currency: {
      chain_id: ChainId.SUI,
      address: null,
    },
    recipient: "0xDestinationWallet",
  });
  ```
</CodeGroup>

### Sale order before and after

<CodeGroup>
  ```typescript v2 theme={null}
  const { data, error } = await apiClient.createSalePayOrder(
    {
      intent: {
        amount: {
          fiat: {
            amount: 49.99,
            unit: "USD",
          },
        },
      },
      metadata: {
        order_id: "order_123",
      },
    },
    process.env.COIN_VOYAGE_API_SECRET!
  );
  ```

  ```typescript v3 theme={null}
  const { data, error } = await apiClient.createSaleOrder(
    {
      amount: "49.99",
      fiat_unit: "USD",
      metadata: {
        order_id: "order_123",
      },
    },
    process.env.COIN_VOYAGE_API_SECRET!
  );
  ```
</CodeGroup>

## Update PayButton

Use `orderId` for server-created orders. Client-side deposit buttons still use `toChain`, `toToken`, `toAmount`, and `toAddress`.

<CodeGroup>
  ```tsx v2 theme={null}
  <PayButton.Custom
    payId={payId}
    onPaymentStarted={(event) => {
      console.log("Payment started", event);
    }}
    onPaymentCompleted={(event) => {
      console.log("Payment completed", event);
    }}
  >
    {({ show }) => <button onClick={show}>Pay</button>}
  </PayButton.Custom>
  ```

  ```tsx v3 theme={null}
  <PayButton.Custom
    orderId={orderId}
    onAwaitingPayment={(event) => {
      console.log("Awaiting payment", event);
    }}
    onConfirmingPayment={(event) => {
      console.log("Payment confirming", event);
    }}
    onExecutingPayment={(event) => {
      console.log("Payment executing", event);
    }}
    onPaymentCompleted={(event) => {
      console.log("Payment completed", event);
    }}
  >
    {({ show }) => <button onClick={show}>Pay</button>}
  </PayButton.Custom>
  ```
</CodeGroup>

The split callbacks give you clearer UI states:

| v3 callback           | When it fires                                                      |
| --------------------- | ------------------------------------------------------------------ |
| `onAwaitingPayment`   | Payment details are available and the user can fund the order.     |
| `onConfirmingPayment` | The source transaction is detected and waiting for confirmation.   |
| `onExecutingPayment`  | CoinVoyage is executing the destination transfer or contract call. |
| `onPaymentCompleted`  | The order completed successfully.                                  |
| `onPaymentBounced`    | The destination call reverted and funds were refunded.             |

## Update provider configuration

`PayKitProvider` no longer documents the v2 `theme` preset prop or `options.embedGoogleFonts`. Use `mode` and `customTheme` instead.

<CodeGroup>
  ```tsx v2 theme={null}
  <PayKitProvider
    apiKey={process.env.NEXT_PUBLIC_COIN_VOYAGE_API_KEY!}
    theme="midnight"
    options={{
      language: "en",
      embedGoogleFonts: true,
    }}
  >
    {children}
  </PayKitProvider>
  ```

  ```tsx v3 theme={null}
  <PayKitProvider
    apiKey={process.env.NEXT_PUBLIC_COIN_VOYAGE_API_KEY!}
    mode="dark"
    customTheme={{
      "--ck-primary-button-background": "#d6296f",
      "--ck-primary-button-color": "#ffffff",
    }}
    options={{
      language: "en-US",
    }}
  >
    {children}
  </PayKitProvider>
  ```
</CodeGroup>

For Sui wallet configuration, rename `config.sui.rpcUrl` to `config.sui.grpcUrl`:

```tsx theme={null}
<WalletProvider
  config={{
    sui: {
      grpcUrl: "https://fullnode.mainnet.sui.io:443",
    },
  }}
>
  {children}
</WalletProvider>
```

## Update swaps

Standalone swaps still use `swapQuote()` for the quote step. Use `swapExecute()` for execution data.

<CodeGroup>
  ```typescript v2 theme={null}
  const { data, error } = await apiClient.swapData(params);
  ```

  ```typescript v3 theme={null}
  const { data, error } = await apiClient.swapExecute({
    trade_type: "EXACT_INPUT",
    amount: "100",
    source: {
      currency: {
        chain_id: ChainId.BASE,
        address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      },
      sender_address: "0xSenderWallet",
      slippage_bps: 50,
    },
    destination: {
      currency: {
        chain_id: ChainId.SUI,
        address: null,
      },
      recipient: "0xDestinationAddress",
    },
  });
  ```
</CodeGroup>

## Update off-ramp flows

v3 names the fiat payout workflow as off-ramp verification, bank accounts, and off-ramp intents.

| v2 method                             | v3 method                                      |
| ------------------------------------- | ---------------------------------------------- |
| `createKYCLink(params, apiSecret)`    | `createOffRampVerification(params, apiSecret)` |
| `getKYCStatus(apiSecret)`             | `getOffRampVerificationStatus(apiSecret)`      |
| `listWithdrawals(apiSecret)`          | `listOffRampIntents(apiSecret)`                |
| `createWithdrawal(params, apiSecret)` | `createOffRampIntent(params, apiSecret)`       |

Bank-account helpers remain part of the off-ramp workflow: `listBankAccounts(apiSecret)`, `getBankAccount(bankAccountId, apiSecret)`, and `addBankAccount(params, apiSecret)`.

## Update webhooks and WebSockets

v3 webhook and `/v3/ws` deliveries use the same event envelope:

```json theme={null}
{
  "event": "ORDER_COMPLETED",
  "delivered_at": "2026-06-23T12:34:56Z",
  "order": {
    "id": "cabc1234567890abcdef12",
    "mode": "SALE",
    "status": "COMPLETED",
    "metadata": {
      "order_id": "order_123"
    }
  }
}
```

Update handlers that previously read lowercase `type` values such as `payorder_completed`.

<CodeGroup>
  ```typescript v2 theme={null}
  switch (payload.type) {
    case "payorder_completed":
      await fulfill(payload.payorder_id);
      break;
  }
  ```

  ```typescript v3 theme={null}
  switch (payload.event) {
    case "ORDER_COMPLETED":
      await fulfill(payload.order.id);
      break;
  }
  ```
</CodeGroup>

Use these v3 event identifiers when subscribing to or dispatching lifecycle events:

* `ORDER_CREATED`
* `ORDER_AWAITING_PAYMENT`
* `ORDER_CONFIRMING`
* `ORDER_EXECUTING`
* `ORDER_COMPLETED`
* `ORDER_ERROR`
* `ORDER_REFUNDED`
* `ORDER_EXPIRED`
* `ORDER_PARTIAL_PAYMENT`

## Test before switching production traffic

Before you route production users to v3:

* Create a `DEPOSIT` order and verify the destination wallet receives funds.
* Create a server-side `SALE` order and render it with `orderId`.
* Verify `ORDER_COMPLETED`, failed, expired, refunded, and partial-payment webhook handling.
* Run a refund through `createRefundOrder()`.
* Run `swapQuote()` and `swapExecute()` if your product exposes standalone swaps.
* Run the off-ramp verification and off-ramp intent flow if you use fiat payouts.
* Confirm your webhook handler is idempotent by `event`, `delivered_at`, `order.id`, and your internal ID in `order.metadata`.

## Reference pages

* [v3 API overview](/api/overview)
* [v3 ApiClient reference](/sdk/apiclient)
* [v3 PayButton reference](/sdk/paybutton)
* [v3 Webhook events](/webhooks/events)
* [v2 frozen docs](/v2/introduction)
