Skip to main content
When a send times out or the connection drops, you can’t tell whether the transaction reached the chain. Retrying might send it twice, and not retrying might lose it. Starting from SDK version 5.2.0, you can attach an idempotency key to a send. Portal broadcasts a key at most once while it remembers the key (at least 24 hours), so a retry with the same key within that time cannot send the same transaction twice. Idempotency keys are optional. Requests without one behave exactly as before. The SDK never generates a key, and apart from the presignature fallback it never retries a request on its own.

Supported requests

Portal enforces the key whether the transaction is signed on the device or through the Enclave MPC API (useEnclaveMPCApi). See Requests that don’t take a key for everything else.

Import the helpers

The idempotency helpers are exported by @portal-hq/utils, which @portal-hq/core depends on. @portal-hq/core doesn’t re-export them.
If your package manager only lets you import packages listed in your app’s package.json, add @portal-hq/utils at the same version as @portal-hq/core.

Generate a key

generateIdempotencyKey() returns a lowercase UUID v4. Generate one key per operation, such as one transfer or one payment, and store it with the pending operation before you send. A retry, even one after the app restarts, can then reuse it.
generateIdempotencyKey() uses crypto.randomUUID or crypto.getRandomValues when the JavaScript runtime provides one. Hermes provides neither, so without a polyfill the key is built from Math.random, which keeps keys unique in practice but is not a cryptographic source. Install a polyfill such as react-native-get-random-values and import it in your app’s entry file before you generate keys.
You can also use your own key, for example an ID from your backend. The key must be a string. The SDK trims surrounding whitespace, and the key must then follow these rules: A key that breaks these rules throws a PortalIdempotencyError with reason set to 'invalidKey' before the approval step, so nothing is signed or sent. The error message names the rule that failed and never contains the key. Check for this error with PortalIdempotencyError.is(error), not instanceof. The error is identified by a cross-copy brand, so instanceof returns false when your dependency tree resolves two copies of @portal-hq/utils.

Send a transaction with a key

Build the transaction once, then send it with the key. Keep the transaction object together with the key, because a retry must send the same transaction:
The same applies on Solana with PortalRequestMethod.SolSignAndSendTransaction or PortalRequestMethod.SolSignAndConfirmTransaction and a solana: chain ID. You can combine idempotencyKey with the other RequestOptions, such as signatureApprovalMemo.

Retry with the same key

If a send fails and you don’t know whether the transaction went out, for example after a timeout, call portal.request(...) again with the same method, chain ID, transaction object and key. One of two things happens:
  • The first attempt never reached Portal, so Portal has no record of the key, and the retry sends the transaction.
  • Portal already recorded the key, whether that attempt is still in progress, was broadcast or failed, and it rejects the retry instead of broadcasting again.
Portal recognizes a retry only when its chainId, method and params match the first request’s, and it compares params as the JSON string the SDK sends. Retry with the same transaction object: the same fields in a different order serialize differently, and the retry can be rejected with IDEMPOTENCY_KEY_REUSED. Reuse a key only for the identical request. If anything changes, such as the amount, the recipient, the chain or the method, generate a new key.

Handle a rejection

Portal’s rejections are thrown as a PortalMpcError whose id is one of the IDs below. Use these helpers from @portal-hq/utils to recognize them: Branch on id to decide what to do next. Each ID is also available as a constant on PortalIdempotencyErrorId:
No rejection returns the original transaction hash. Before you send an operation again under a new key, check the chain or your transaction history to confirm the first attempt didn’t land. Portal treats a new key as a new transaction.
Match rejections with isIdempotencyRejection() or on id, not on the error message or code.

Retry sendAsset

Pass the key in SendAssetParams:
sendAsset builds the transaction again on every call, and Portal recognizes a retry only when the rebuilt transaction matches the first one:
  • EVM: the build carries no nonce or gas, so with unchanged inputs the rebuilt transaction normally matches. A retry gets the status of the first request, as described in Handle a rejection.
  • Solana: each build carries a new recent blockhash, so a retry with the same key is usually rejected with IDEMPOTENCY_KEY_REUSED, which doesn’t tell you whether the first transfer is in progress, completed or failed.
Either way, while Portal remembers the key, the transfer is never broadcast twice under it. Before you send the transfer again under a new key, wait until the first one can no longer land, then check the chain to confirm it didn’t:
  • Solana: wait until the first transfer is confirmed or its recent blockhash has expired, usually within about 60–90 seconds after the transaction was built.
  • EVM: wait until the first transfer is confirmed or the account’s nonce has moved past it.
For the most predictable retries, especially on Solana, build the transaction once with portal.api.buildTransaction and send it with portal.request(...) as shown in Send a transaction with a key. Every retry then gets the status of the first request, including IDEMPOTENT_REQUEST_IN_PROGRESS.

Requests that don’t take a key

The Enclave MPC API also accepts keys for Tron and Stellar broadcasts. The React Native SDK doesn’t take a key for them: sendAsset doesn’t support Tron or Stellar chains, and a key passed to portal.request with any method not listed under Supported requests is ignored with a warning.

How long Portal remembers a key

Portal remembers a key for at least 24 hours after the request was last updated, then removes it. Once the key is removed, the same key is treated as a new request. Because the exact removal time isn’t fixed, check the chain before you retry an operation that is older than 24 hours, whatever key you use.

Presignatures

If you enable presignatures (usePresignatures in featureFlags), the presignature attempt carries the same key as the request. Presignatures apply only to SECP256K1 signing, so this affects EVM sends.
  • If Portal rejects the presignature attempt because of its key, the SDK throws that rejection and doesn’t fall back to a normal sign.
  • If the presignature attempt fails for another reason, the SDK falls back to one normal sign with the same key and request. If the failed attempt already reached Portal, Portal can reject the fallback, for example with IDEMPOTENT_REQUEST_IN_PROGRESS, where a request without a key would have been signed. The SDK then logs a warning that the presignature attempt may already have used the key.
Handle either case like any other rejection.

Next Steps