@getpara/rest-sdk
v3.28.0
Published
Typed server-side SDK for Para REST wallet management and signing
Readme
@getpara/rest-sdk
Typed REST client for Para server-to-server wallet operations.
Use this package when your backend owns the workflow and signs with Para REST API keys. Do not ship it to browsers,
React apps, mobile apps, wallets, or any other user-controlled runtime. It sends X-API-Key on every request.
Use @getpara/rest-sdk for API-key-backed pregen or programmatic wallets. Use @getpara/server-sdk when you need
Para's share-backed server SDK flows, imported user sessions, or core SDK behavior. Use Web, React, React Native,
Swift, or Flutter SDKs for user-facing authentication and wallet UX.
The core client covers Para's public REST wallet surface:
- wallet creation, listing, lookup, and identifier updates
- SDK pregen share migration and enclave public-key lookup
- balances, signing, transfers, fee estimation, faucet requests, and REST transaction history
- REST-specific headers and behavior:
X-API-Key,X-Request-Id, caller-suppliedIdempotency-Key,AbortSignal, environment selection, fetch injection, JSON serialization errors, validation errors, and HTTP error mapping
The adapter subpaths expose the signing subset through ethers v6, viem v2, and Solana v2 signer interfaces.
Install
yarn add @getpara/rest-sdkInstall only the peer dependencies for the adapters you use:
yarn add ethers
yarn add viem
yarn add @solana/addresses @solana/keys @solana/signers @solana/transactionsThe core client expects Node.js 18+ or an injected fetch.
Client
import { ParaRestClient, ParaRestError } from '@getpara/rest-sdk';
const para = new ParaRestClient({
apiKey: process.env.PARA_API_KEY!,
env: 'BETA',
});
const wallet = await para.createWallet({
type: 'EVM',
userIdentifier: '[email protected]',
userIdentifierType: 'EMAIL',
});
const signature = await para.signMessage(wallet.id, { message: 'hello' });env accepts PROD, BETA, SANDBOX, or { baseUrl }. Pass fetch to inject your runtime's fetch implementation,
pass requestIdGenerator if you want to control X-Request-Id values, and pass AbortSignal in call options to
cancel in-flight requests.
ParaRestError is thrown for non-2xx HTTP responses and includes status, code, requestId, and the parsed response
body. ParaRestValidationError is thrown before fetch for missing local required values. ParaRestSerializationError
is thrown before fetch when a request body cannot be JSON serialized.
try {
await para.getWallet(wallet.id, { signal: AbortSignal.timeout(10_000) });
} catch (error) {
if (error instanceof ParaRestError) {
console.error(error.status, error.code, error.requestId, error.body);
}
}Signing Behavior
EVM message and typed-data signing send unhashed payloads to REST. Para hashes EIP-191 and EIP-712 payloads server-side
before signing. The ethers adapter follows ethers' own typed-data behavior and omits any caller-provided EIP712Domain
entry from types before forwarding to REST.
The viem adapter follows viem's signMessage split: string messages are forwarded to REST sign-message, while
byte-like messages are hashed locally and forwarded to REST sign-raw. Pass { raw: bytes } when a 0x... string
should be treated as bytes instead of text.
EVM transaction signing returns the REST API's signed transaction response. The SDK normalizes the caller's transaction
request before sending it, but does not re-derive the signed payload locally or diff every returned field against the
request. REST EVM transaction signing supports ordinary legacy and EIP-1559 sends only; contract deployments, access
lists, blob transactions, and authorization-list transactions are rejected before calling REST. The viem adapter accepts
a chain serializer whose output equals the standard Ethereum serialization, such as on Base and other OP Stack chains,
or for standard Celo and ZKsync transactions. Chain-specific transactions, such as OP Stack deposits, Celo feeCurrency
transactions, and ZKsync EIP-712 transactions, are rejected before calling REST.
The Solana adapter implements the Solana v2 signer traits by signing message bytes through sign-raw. It returns
signature dictionaries and can merge signatures into Solana v2 message and transaction objects. Use
para.signTransaction(..., { broadcast: true }) or para.transfer(...) when your backend wants REST to submit a
transaction and return a transactionId.
Idempotency-Key is caller-supplied per request:
await para.createWallet(
{
type: 'EVM',
userIdentifier: '[email protected]',
userIdentifierType: 'EMAIL',
},
{ idempotencyKey: crypto.randomUUID() },
);Use an idempotency key when a retry could create duplicate state or submit duplicate signing/broadcast work. The SDK does not generate one for you because it does not retry requests internally.
EVM fee urgency
Set urgency: 'normal' | 'urgent' on a self-funded EVM transfer, transferWithAutoRetry,
speedUpRestTransaction, cancelRestTransaction, createYieldDeposit, or createYieldWithdrawal request.
Omission means normal. Para selects fresh recommended prices; the SDK forwards the choice without calculating fees.
await para.transfer(wallet.id, {
to: '0x1111111111111111111111111111111111111111',
value: '1000000000000',
chainId: '11155111',
urgency: 'urgent',
}, { idempotencyKey: 'saved-payment-key' });Explicit initial prices, including zero, stay pinned. Urgency changes generated prices, not gas units; an inconsistent
fee pair is rejected. Urgent does not guarantee confirmation time. Sponsored transfers, non-EVM execution, and
caller-built signTransaction/signTransactionWithAutoRetry requests reject urgency. EVM fee estimates return
both options and do not take a selector. estimateFee() exposes typed feeOptions.normal and feeOptions.urgent,
with their fee-family fields, execution budgets, quote timestamp and block number. These optional fields are absent
on non-EVM or older responses.
Durable recovery and yield retain the choice across worker restarts and approval/action phases. Omitted and explicit normal share the legacy durable identity; changing urgency under the same key conflicts. Replays retain prepared and signed bytes. Fee ceilings and deadlines remain unchanged; urgent never enlarges spending authorization.
Transaction History
When REST broadcasts a transaction, signTransaction(..., { broadcast: true }) and transfer(...) return a
transactionId. Store that id with your own order, payout, or job record so you can reconcile the broadcast later.
const result = await para.signTransaction(wallet.id, {
transaction,
broadcast: true,
});
if (result.transactionId) {
const record = await para.getRestTransaction(wallet.id, result.transactionId);
console.log(record.status, record.hash);
}
const recent = await para.listRestTransactions(wallet.id, {
status: 'confirmed',
limit: 10,
});Webhooks are the preferred way to receive asynchronous status changes after a broadcast. Treat the REST SDK history
methods as the read model: fetch the record immediately after a broadcast, backfill missed webhook deliveries, or
reconcile your database against Para's persisted transaction state. See the Para webhook docs for signature
verification and rest.transaction.confirmed / rest.transaction.failed payload examples.
Automatic retry for EVM transfers
Use transferWithAutoRetry for self-funded native/ERC-20 EVM transfers. Save the body and key before sending;
reuse both after a timeout. HTTP 202 means durable admission before signing or broadcast and returns the original
transaction ID and autoRetry state instead of signed bytes.
const key = crypto.randomUUID();
const request = {
to: '0x1111111111111111111111111111111111111111',
value: '1000000000000',
chainId: '11155111',
urgency: 'urgent' as const,
autoRetry: {
maxExecutionFeeWei: '1000000000000000',
expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
},
};
// Persist { key, request } before submitting. Never renew the deadline on replay.
const accepted = await para.transferWithAutoRetry(wallet.id, request, { idempotencyKey: key });
const current = await para.getRestTransaction(wallet.id, accepted.transactionId);
console.log(current.status, current.autoRetry?.state);maxExecutionFeeWei is the per-attempt execution-gas ceiling, excluding separate chain fees. Initial fees must fit;
later recommendations remain subject to the original ceiling and historical replacement minimum. This SDK method
supports EVM transfers; it does not add urgency to Solana recovery. See Automatic fee retry.
Manual EVM recovery
For an eligible unresolved ordinary self-funded EVM transfer, use speedUpRestTransaction or
cancelRestTransaction with a new saved idempotency key and a replacement fee ceiling:
const recovery = await para.speedUpRestTransaction(wallet.id, transactionId, {
maxExecutionFeeWei: '1000000000000000',
urgency: 'urgent',
}, { idempotencyKey: recoveryKey });
console.log(recovery.manualRecovery.state, recovery.manualRecovery.attempt.hash);Both methods return the original transaction ID and typed manualRecovery metadata. Read progress with
getRestTransaction. Use cancelRestTransaction with the same body shape to request cancellation. Only one manual
speed-up or cancellation is accepted per original transfer; HTTP acceptance does not prove inclusion or cancellation.
The original payment can still win. Replaying the saved key/body keeps the selected replacement bytes.
See Manual EVM recovery for eligibility.
Automatic retry for broadcast signing
Use signTransactionWithAutoRetry with autoRetry and broadcast: true to authorize recovery of one EVM transaction or supported legacy Solana
transaction. Save the key and entire request before submitting; HTTP retries reuse both, including the original deadline.
The server returns HTTP 202 before signing or broadcasting, with transactionId, status, and autoRetry instead of signed bytes.
const key = crypto.randomUUID();
const request = {
transaction: { ...transaction, type: 2 as const },
broadcast: true as const,
autoRetry: {
maxExecutionFeeWei: '1000000000000000',
expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
},
};
// Persist { key, request } in your job record before this call.
const result = await para.signTransactionWithAutoRetry(wallet.id, request, { idempotencyKey: key });
const record = await para.getRestTransaction(wallet.id, result.transactionId);
console.log(record.status, record.autoRetry?.state, record.monitoring?.winningHash);EVM supports type 0 and 2 contract calls, including type 2 access lists. Fees and gas limits use decimal integer
strings. Same-nonce replacements preserve the destination, value, calldata, gas limit and access list; fee caps
increase by at least 12.5%, rounded up, at least 60 seconds after the last broadcast. There is no fixed attempt limit.
maxExecutionFeeWei caps each attempt's execution gas; separate chain fees are excluded.
For Solana, send an unsigned legacy transaction with the wallet as the sole signer and fee payer, a still-valid finalized
recent blockhash, exactly one 120000-unit compute limit and one compute-unit-price instruction. Use maxExecutionFeeLamports
instead of maxExecutionFeeWei; it caps base plus priority fees and excludes account rent. Versioned and durable-nonce
transactions and unsupported system/account instructions are rejected. Para must locate the original hash in its
512-slot finalized history window; missing or older anchor history is rejected before signing. The original transaction
bytes are preserved even when a newer finalized hash exists. Solana resends exact bytes until expiry and
requires finalized non-execution proof before rebuilding with a new blockhash.
The enrolling secret key, wallet and current policies must authorize every attempt. A transaction-bound approval
does not authorize changed fees: retry requests do not accept reviewId or initiatorUserId. A stopped retry can
still have a pending on-chain transaction. Continue polling its original ID; do not submit the operation again with a
new key. Signer adapters keep their sign-only behavior. See Automatic fee retry.
Yield deposits and withdrawals
Use createYieldDeposit, createYieldWithdrawal and getYieldOperation for the existing Aave/Morpho yield routes.
Save the complete request and idempotency key before the first call. Amounts are USDC decimal units; withdrawals
also accept 'all'. Select a supported marketId, or use chainId to select its Aave market.
const key = crypto.randomUUID();
const request = {
marketId: 'aave-v3:11155111:usdc',
amount: '10',
autoRetry: {
expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
approval: { maxExecutionFeeWei: '200000000000000' },
action: { maxExecutionFeeWei: '1000000000000000' },
},
};
// Persist { key, request } before submitting; replay both unchanged.
const accepted = await para.createYieldDeposit(wallet.id, request, { idempotencyKey: key });
const operation = await para.getYieldOperation(wallet.id, accepted.id);
console.log(operation.status, operation.transactions, operation.autoRetry?.phases);autoRetry is optional. Its required action ceiling authorizes one deposit/withdrawal transaction, including all
same-nonce fee replacements. A needed approval requires its own explicit ceiling; Para never copies the action
ceiling or rolls unused authorization forward. Include approval authorization on deposits unless existing allowance
is known to cover the amount; withdrawals never need it. Each ceiling uses the existing
per-transaction execution-gas semantics, excluding separate chain charges. This is not a cumulative operation budget.
A missing approval ceiling or a first transaction above its ceiling cannot be fixed by replaying the same request.
Cancel the proven-unsigned operation first with
POST /v1/wallets/{walletId}/yield/operations/{operationId}/cancel, then use a new key and adequate ceilings.
The SDK does not wrap this cancellation route. Signed or uncertain phases remain tracked and cannot be replaced this way.
The original deadline applies across approval, action and every replay. If it expires after approval, the confirmed
approval remains visible and the unsigned action does not run. autoRetry.phases exposes each phase's transaction
ID, attempts and retry state. An initial reviewed transaction retains its exact bytes; replacements require a fresh
policy allow and reuse that phase's principal accounting. Current policy and accounting must be re-proven;
incompatible reuse, including unsupported approved spend overage, stops signing before MPC. A stopped retry can still
be pending on-chain: track the
same operation and transaction IDs until resolved. Omission retains existing exact-byte recovery.
Sponsored Transfers
Set sponsoredGas: true to require Para-paid gas on an eligible EVM token transfer. Para must enable your
partner's existing admin-managed featureFlags.restGasSponsorship setting. If sponsorship cannot be honored, the
request fails with an actionable error; it never falls back to wallet-paid gas. Accepted sponsored transfers return HTTP 202 with
sponsoredGas: true, a transactionId, and a status. The response has no signedTransaction.
For a common transfer, provide the recipient, amount and asset (chain and token) with sponsoredGas: true;
Para chooses the nonce, gas and fees. If you require an explicit nonce, fee cap or other control, the SDK forwards
it unchanged. The current sponsorship API rejects unsupported controls with an actionable error. The SDK never
drops those controls, flips sponsoredGas to false, or retries through wallet-paid execution.
Set sponsoredGas: false or omit it to require wallet-paid gas, regardless of partner sponsorship eligibility.
Only sponsoredGas: true requests Para-paid gas. Transfer responses and transaction
history/detail include sponsoredGas: true for Para-paid gas or false for wallet-paid gas. Older API versions may omit it;
the deprecated response-only executionMode field remains for compatibility ('sponsored' or 'legacy'). Do not send
executionMode in requests: the API rejects it with an actionable error.
The server must support sponsoredGas before you use it. The SDK expects wallet-paid gas when the field is omitted
and leaves the request unchanged. If a server returns a different sponsorship choice, the SDK
throws ParaRestSponsorshipError and preserves the result in error.response. For older responses, the SDK infers
sponsorship from executionMode === 'sponsored'. A transaction may already have been submitted: reconcile its tracking
ID/hash before retrying. This response check cannot undo a broadcast.
Upgrading an integration that previously relied on partner enrollment alone changes its gas-payment contract:
send sponsoredGas: true on each new transfer that requires Para-paid gas. Omitted requests now require wallet-paid
gas, and the SDK rejects an unexpected sponsored response, including an older response without the Boolean.
Replaying an existing sponsored key requires sponsoredGas: true and unchanged economic intent. Replaying that key
with false or an omitted flag returns 409 IDEMPOTENCY_CONFLICT without creating another transfer. Recover the
original tracking ID or replay the original sponsored request; never retry the payout with a fresh idempotency key.
import { isSponsoredTransfer } from '@getpara/rest-sdk';
// Save this key with your payout before the request. Reuse it if the response is lost.
const requestKey = crypto.randomUUID();
const result = await para.transfer(
wallet.id,
{ to: recipient, value: '1000000', chainId, tokenAddress, kind: 'ERC20', sponsoredGas: true },
{ idempotencyKey: requestKey },
);
if (isSponsoredTransfer(result)) {
// Save the tracking ID, then poll this lookup from your background job.
const record = await para.getRestTransaction(wallet.id, result.transactionId);
console.log(record.status, record.stage, record.hash, record.actualGasCost);
} else {
console.log(result.signedTransaction, result.txHash);
}isSponsoredTransfer() narrows the response for tracking or reading signed bytes. It uses sponsoredGas when present
and falls back to the older response-only executionMode field when the Boolean is absent.
Sponsored requests require an idempotency key and a positive integer value in token base units. Sponsorship manages
nonce, gas, fees and submission. The only accepted body fields are to, value, chainId, tokenAddress, kind, broadcast,
addressScreening, and sponsoredGas. All other fields are rejected, even when null or empty, including autoRetry,
nonce/type/fee overrides, reviewId, initiatorUserId, and network. The generic TransferRequest still exposes
initiator and network fields for legacy transfers. Omit kind or set it to 'ERC20'; omit broadcast and
addressScreening or set them to true. Explicit sponsorship does not support Solana or disabled recipient screening.
The server enforces partner eligibility and its supported chain/token configuration.
Unavailable or paused sponsorship rejects explicit sponsored requests. The partner flag controls sponsorship eligibility;
it never selects sponsorship for requests with sponsoredGas: false or an omitted field. Existing sponsored operations retain their tracking IDs.
Poll getRestTransaction until the status is confirmed, reverted, or failed; stop polling for policy approval when
stage is requires_action. submitted with stage included is still awaiting receipt verification. submission_unknown means
the provider may have received the operation. delivery_unverified means the operation was included but token delivery
needs reconciliation; neither stage is a reason to send again. Reuse the original request and key to recover a lost
response, and resume polling the original tracking ID. The SDK does not retry or poll automatically.
Conditional approval policies are not supported for sponsored transfers in this pilot. After an existing policy review is approved, explicitly continue the same operation:
await para.resumeRestTransaction(wallet.id, transactionId, { reviewId });Resume accepts a review ID, not a new transfer. Repeating transfer with the original key only retrieves the existing
operation and does not resume it. userOperationHash identifies the sponsored operation; txHash on the transfer
response and hash in transaction history identify its on-chain transaction once included. actualGasCost is a decimal
string in the chain's native-token base units after settlement, including included reverts. Keep it as a string or
convert it to bigint, not number. Use getRestTransaction() for sponsorship stage, review details, user-operation hash,
and settled gas cost; listRestTransactions() returns transaction summaries. Fetch persisted status to reconcile missed webhooks.
On-Ramp Purchases
getOnRampPurchase looks up a fiat on-ramp purchase by the id the SDK reports on the ONRAMP_LAUNCHED event — the
same id that arrives as purchaseId on the onramp.completed webhook.
const purchase = await para.getOnRampPurchase(purchaseId);
if (purchase.status === 'FINISHED') {
console.log(purchase.transactionHash, purchase.asset.symbol, purchase.assetQuantity);
}The response is the onramp.completed payload plus a lifecycle status. A purchase that hasn't settled returns
partial data — transactionHash and completedAt are null until status is FINISHED — so branch on status
rather than inferring completion from the hash, and treat an unrecognized status as non-terminal.
As with transaction history, webhooks are the preferred way to learn about settlement; use this to backfill a missed
delivery or to check a purchase still in flight. Check testMode before recording a purchase in production
accounting: test purchases run a real quote but are never charged.
Webhook Types
Webhooks are delivered to your backend, so the types for handling them live here. ParaWebhookEvent is a
discriminated union, so a switch on event.type narrows event.data to that event's payload:
import { WebhookEventType, type ParaWebhookEvent } from '@getpara/rest-sdk';
export function handleParaWebhook(event: ParaWebhookEvent) {
switch (event.type) {
case WebhookEventType.ONRAMP_COMPLETED:
// event.data is OnrampCompletedPayload
return credit(event.data.purchaseId, event.data.transactionHash);
case WebhookEventType.REST_TRANSACTION_CONFIRMED:
// event.data is RestTransactionConfirmedPayload
return settle(event.data.transactionId, event.data.hash);
}
}Use ParaWebhookEvent rather than the generic WebhookEvent, whose type and data are uncorrelated — data
stays the union of every payload regardless of type. Individual payload interfaces (OnrampCompletedPayload,
WalletCreatedPayload, …), the WebhookEventPayloadMap, and ParaWebhookPayload<K> are exported too, for typing a
single-event handler.
These are types only — this package does not receive, verify, or dispatch webhooks. Verify the signature on the raw request body before trusting a delivery; see the Para webhook docs for the procedure.
Adapters
import { createParaRestEthersSigner } from '@getpara/rest-sdk/ethers';
import { createParaRestViemAccount } from '@getpara/rest-sdk/viem';
import { createParaRestSolanaSigner } from '@getpara/rest-sdk/solana';
const ethersSigner = createParaRestEthersSigner({
client: para,
walletId,
address,
});
const viemAccount = createParaRestViemAccount({
client: para,
walletId,
address: address as `0x${string}`,
});
const solanaSigner = createParaRestSolanaSigner({
client: para,
walletId: solanaWalletId,
address: solanaAddress,
});Unsupported Surfaces
This package intentionally does not include browser connectors, React hooks, wagmi or RainbowKit connectors, mobile
auth, user session flows, account creation UI, MPC share storage, share-migration encryption helpers, legacy adapters,
or automatic retries. Use @getpara/server-sdk for migrateWalletShare. Keep API keys on your backend and expose
only your own application-specific routes to clients.
Sandbox Probe Fixture
scripts/probe-signature-format.ts can probe sandbox signature formats when PARA_API_KEY is available. The sanitized
fixture in src/__fixtures__/signature-formats.json documents observed behavior for regression tests. It is not a
replacement for the OpenAPI contract or the live REST router behavior.
