Withdraw funds

Choose a live withdrawal route, withdraw instantly or by request, and confirm the payout reached the customer's wallet.

A withdrawal spends shares from a position and pays the deposit token back to the wallet that owns it. Some routes pay in the same transaction; others pay later. Read the live routes first, then follow the one your customer chooses.

Set SDP_API_URL and SDP_API_KEY as shown in the Quickstart.

1. Choose a route

POST /v1/earn/external-wallet/withdrawal-options reads each route from live provider state.

Shell
curl --fail-with-body -sS "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/external-wallet/withdrawal-options" \
  -H "Authorization: Bearer $SDP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc"}'
JSONResponse (trimmed)
{
  "data": {
    "positionId": "earn_position_0432ffeb-c574-4b11-80dc-a09defb68fcc",
    "instant": true,
    "providerOrder": false,
    "queued": true,
    "queueAsset": {
      "assetMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
      "allowWithdrawals": true,
      "secondsToMaturity": 3600,
      "minimumSecondsToDeadline": 60,
      "maximumSecondsToDeadline": 7776000,
      "minimumDiscountBps": 0,
      "maximumDiscountBps": 100,
      "minimumShares": "0.000001",
      "shareDecimals": 6
    },
    "parRedemption": null
  }
}
RouteWhen it is offeredPays out
Instant withdrawalinstant: trueIn the same transaction.
Provider orderproviderOrder: trueLater, when the provider completes the order.
Solver queuequeued: true, with live terms in queueAssetLater, at a discount, when a solver fulfills the request.
Operator redemptionparRedemption is presentLater, at par, when the operator settles the request.

A position offers an instant withdrawal or a provider order, never both, and the same withdrawal endpoints build whichever it offers. When more than one route is offered, let your customer choose, and keep their choice. Never infer a route from the strategy's liquidityTerm. A failed read or missing terms means the route is unavailable, not that it pays zero.

2. Withdraw instantly

An instant withdrawal works like a deposit: preview, build, sign, submit, and track. Withdraw at most the position's live withdrawableShares: a preview does not check the wallet's balance.

The example uses the Quickstart's earn-api.ts and test-wallet.ts, and floor.ts from Slippage floors. Call withdraw with the positionId and withdrawalSlippage saved in your backend after the deposit, plus the share amount your customer chose:

TypeScriptwithdraw.ts
import { randomUUID } from "node:crypto";
import { earnRequest, waitForMovement } from "./earn-api";
import { floorForTolerance } from "./floor";
import { ownerAddress, signWithTestWallet } from "./test-wallet";

type SavedPosition = {
  positionId: string;
  withdrawalSlippage: { defaultToleranceBps: number } | null;
};
type Position = { id: string; withdrawableShares?: string };

export async function withdraw(savedPosition: SavedPosition, shares: string) {
  let position: Position | undefined;
  let before: string | null = null;
  do {
    const query = new URLSearchParams({ ownerAddress, limit: "100" });
    if (before !== null) query.set("before", before);
    const page: { positions: Position[]; nextCursor: string | null } = await earnRequest(
      `/v1/earn/external-wallet/positions?${query}`,
    );
    position = page.positions.find((p) => p.id === savedPosition.positionId);
    before = page.nextCursor;
  } while (!position && before !== null);
  if (!position || position.withdrawableShares === undefined) {
    throw new Error("The saved position cannot withdraw right now");
  }

  const preview = await earnRequest("/v1/earn/external-wallet/withdrawal-previews", {
    positionId: position.id,
    shares,
  });
  if (preview.blockingIssues.length > 0) {
    throw new Error(`Preview blocked: ${JSON.stringify(preview.blockingIssues)}`);
  }

  // Send a floor only when the saved policy takes one; never require a catalog listing to exit.
  const policy = savedPosition.withdrawalSlippage;
  const minAmountOut = policy
    ? floorForTolerance(preview.assetsOut, preview.assetDecimals, policy.defaultToleranceBps)
    : undefined;

  const { transaction: built } = await earnRequest(
    "/v1/earn/external-wallet/withdrawal-transactions",
    { positionId: position.id, shares, minAmountOut },
  );
  const signedTransaction = await signWithTestWallet(built.transaction);

  const { withdrawal } = await earnRequest(
    "/v1/earn/external-wallet/withdrawals",
    { transactionId: built.transactionId, signedTransaction },
    randomUUID(),
  );
  return waitForMovement(withdrawal.movementId);
}
JSONPreview response
{
  "data": {
    "positionId": "earn_position_e3053c7b-e41e-4f21-b4dc-e229ab95f40b",
    "assetsOut": "10",
    "assetDecimals": 6,
    "blockingIssues": []
  }
}

The preview quotes the payout; it does not reserve it. The build requires minAmountOut when the strategy's withdrawalSlippage is not null, and refuses it when the policy is null. The position supplies the vault and mints, so a delisted strategy stays exitable. Keep the deposit-time policy with the position; do not require the strategy to appear in today's catalog.

If the build reports that the floor policy changed, refresh the policy with your SDP contact before retrying. Never guess a policy from a missing catalog row.

At finalized, tokenAmount is the payout SDP observed. Check the customer's token balance onchain if you need independent proof, then refresh balances.

Provider orders

A provider order takes an exact share amount and pays out after the provider prices and settles it. Its preview may return 501, which does not block the build. No floor can bound a later price, so omit minAmountOut when the policy is null.

The withdrawal's settlement is provider_order. Confirmation proves the order transaction, not the payout: the movement stays confirmed until the provider completes the order. Show it as pending until it reaches finalized; see How a movement settles.

Asynchronous withdrawals

A solver queue or an operator redemption creates a withdrawal request. The request transaction moves shares, never the payout, so track the request until it pays out or is canceled.

Solver queues

The request escrows shares. After secondsToMaturity, a solver may fulfill it at the agreed discount until the deadline. Before the customer confirms, show the live terms from queueAsset: minimum shares, the discount range, maturity, and the deadline range.

Send shares, discountBps, and deadlineSeconds, each inside the live terms; mechanism defaults to solverQueue. The deadline counts from maturity, not from creation, and both run on the cluster's clock: maturityTimestamp and deadlineTimestamp are onchain Unix times.

  • The discount is your customer's cost. A larger discountBps makes a solver more likely to fill the request, and the customer receives that much less.
  • The deadline is how long solvers have. A longer deadlineSeconds gives them more time, but keeps the shares escrowed longer if none fills it.
  • Open requests leave the balance. Escrowed shares drop out of the position's shares and earnings' currentValue, and earnings report withdrawals_pending. Show the request's quotedAssets as an expected payout.

Operator redemptions

The request converts shares into an intermediate token and authorizes the operator to settle it for the deposit token at par. Read the live parRedemption terms: minimum shares, intermediateMint, assetMint, and whether cancellation is allowed.

Send mechanism: "operatorRedemption" with either shares or intermediateAmount from an existing parIntermediate holding, never both. A null minimumShares means the conversion rate is unavailable: the preview reports a blocking issue.

Create a request

Resolve every blockingIssues entry in the preview, then build, sign, and submit as for an instant withdrawal. Save the returned withdrawalRequestId.

Keep the signed request and its idempotency key until the request resolves. The wallet may already have broadcast it, so SDP accepts it even after its build expires:

Submit answerWhat to do
409 CONFLICT on a late requestSDP released the build's reservation and accepts the request once the exact transaction is finalized. Retry the same bytes and key. For an operator redemption whose cycle already closed, contact support instead.
400 TRANSACTION_FAILEDThe transaction finalized as failed. Build a new request.
503SDP could not finish checking the request's history. Retry the same bytes and key, and contact support if it persists.

Track a request

Poll GET /v1/earn/external-wallet/withdrawal-requests/{withdrawalRequestId} every minute or two. Status values spell it cancelled.

StatusMeaningWhat to show
creatingSDP recorded the signed request; its chain outcome is unresolved.Pending.
pendingWaiting for maturity or for the operator.Pending, with the expected timing.
fulfillableA solver may fulfill it now.Pending.
expiredCancelableThe solver deadline passed without fulfillment.Offer cancellation.
cancellingA cancellation was submitted; a fulfillment can still race it.Canceling.
closedOrUnknownThe request closed, and SDP has not yet proved a payout or a cancellation.Pending; keep tracking.
fulfilledSDP observed the payout. assetsPaid holds the amount.Done.
cancelledSDP observed the cancellation.Canceled; refresh holdings.
failedIt did not happen: SDP decides that only after repeated checks find no trace of it. failureReason says why.Failed.

Only fulfilled, cancelled, and failed are final, and only fulfilled pays out. Never credit quotedAssets, a request transaction, or a cancellation as received funds. A solver queue request that no solver takes stays fulfillable until its deadline, then becomes expiredCancelable, and its shares stay escrowed until the customer cancels it.

On startup and after reconnecting, restore every open request: GET /v1/earn/external-wallet/withdrawal-requests?ownerAddress=<wallet>&settled=false, reading every page.

Sign withdrawal requests and cancellations without broadcasting them, and submit them through SDP promptly. If a wallet broadcasts one itself and SDP first sees it after the build expires, SDP may not be able to track it.

Cancel a request

Offer cancellation for a solver queue request only at expiredCancelable, never from a client clock; building one earlier returns 409 CONFLICT. An operator redemption can be canceled while pending if its live terms allow it. Refresh the request first: a fulfillment can race the cancellation.

Both steps take earn.manage, and cancellation stays available after production access is removed or new deposits are disabled. Use a new Idempotency-Key for the cancellation. A canceled solver request returns the escrowed shares; a canceled operator redemption leaves the intermediate token, not the original shares. Refresh holdings before another withdrawal.