Processing Withdrawals with V2 Wallets

This guide covers processing withdrawal transactions using V2 withdrawal wallet using the transaction level submit flow.

This guide covers processing withdrawal transactions using V2 withdrawal wallet using the transaction level submit flow.

Prerequisite: Wallet setup is completed and a withdrawal wallet is created.

Overview

Processing a withdrawal on a V2 wallet moves through approval and firewall checks before a transaction is ready to submit for signing. At that point, you choose exactly which pending transaction moves forward next — you're not forced to submit everything pending on the wallet at once.

The sections below walk through the full process: where submission fits in the withdrawal lifecycle, the end-to-end integration flow, tracking status along the way, and the submit call itself.


Where this fits in the withdrawal lifecycle

stateDiagram-v2
    [*] --> awaiting_approval
    awaiting_approval --> awaiting_firewall_approval: internal approvals clear
    awaiting_approval --> failed: rejected by approver
    awaiting_firewall_approval --> firewall_finalised: firewall/policy checks pass
    awaiting_firewall_approval --> failed: rejected in firewall / policy checks failed
    firewall_finalised --> awaiting_signatures: ready to submit
    awaiting_signatures --> signing_in_progress: you call POST /api/v2/transactions/submit
    signing_in_progress --> fully_signed: signing completes
    signing_in_progress --> failed: signing does not complete
    fully_signed --> awaiting_broadcast: queued for broadcast
    awaiting_broadcast --> awaiting_confirmation: broadcast to the network
    awaiting_confirmation --> awaiting_finalisation: confirmations accumulating
    awaiting_finalisation --> completed: required confirmations reached
    awaiting_confirmation --> failed: reverted / dropped
    completed --> [*]
    failed --> [*]

A transaction becomes eligible for submit once it reaches awaiting_signatures. That's the only state this endpoint accepts for a fresh submit.


Integration flow

sequenceDiagram
    participant You as Your platform
    participant Liminal as Liminal API

    You->>Liminal: POST /api/v2/wallets/:walletId/send-many-transaction-request (create withdrawal, sequenceId)
    Liminal-->>You: 201 Created — transaction enters awaiting_approval

    Note over Liminal: Async: balance check, address validation, firewall/policy checks

    loop poll until ready
        You->>Liminal: POST /api/wallet/transaction-status
        Liminal-->>You: legacyStatus, primaryStatus, secondaryStatus
    end

    Note over You,Liminal: secondaryStatus reaches awaiting_signatures

    You->>Liminal: POST /api/v2/transactions/submit { walletId, sequenceIds: ["seq-A"] }
    Note over You,Liminal: Signs and submits
    Liminal-->>You: 200 OK — { processed: ["seq-A"], failed: [] }

    loop poll until final
        You->>Liminal: POST /api/wallet/transaction-status
        Liminal-->>You: legacyStatus=4 / primaryStatus="success" (done), or legacyStatus=5 / primaryStatus="failed"
    end

Steps, in words:

  1. Create the withdrawal. Call POST /api/v2/wallets/:walletId/send-many-transaction-request with a unique sequenceId.
  2. Wait for pre-signing checks. Liminal runs balance, address, and policy/firewall checks asynchronously.
  3. Poll the status of the transaction. Use POST /api/wallet/transaction-status and wait for legacyStatus: 1, primaryStatus: "pending", secondaryStatus: "awaiting_signatures" — this means the transaction is ready to be submitted.
  4. Submit it. Call POST /api/v2/transactions/submit with walletId and the sequenceId you want to move forward.
  5. Read the result. The response tells you whether the sequenceId was processed or failed, with a reason if it failed. Nothing further to sign.
  6. Confirm the outcome. Either keep polling transaction status until legacyStatus reaches 4 (primaryStatus: "success") or 5 (primaryStatus: "failed"), or rely on a transaction webhook instead — polling isn't required if you've configured one. Use legacyStatus/primaryStatus for this final check rather than secondaryStatus — the granular broadcast/confirmation sub-statuses aren't yet consistent across every chain.

You can also submit more than one transaction in a single call — see Submitting more than one transaction at a time below.


Tracking status: transactionStatus

POST /api/wallet/transaction-status returns an additive transactionStatus object alongside the existing numeric status field. This is purely additive — no existing field changes shape.

Polling isn't the only option — you can also use a transaction webhook to get notified when a transaction reaches its final state, instead of polling for it.

Request — POST /api/wallet/transaction-status

{ "wallet": { "coin": "eth", "walletId": 14999 }, "sequenceId": "08d5893b-0066-4619-b7af-a5bae54205d5" }

Response — 200 OK

{
  "success": true,
  "data": {
    "status": 1,
    "transactionStatus": {
      "legacyStatus": 1,
      "primaryStatus": "pending",
      "secondaryStatus": "signing_in_progress"
    }
  }
}

All other fields on data are unchanged. transactionStatus.legacyStatus always equals the existing numeric status field — it's a convenience duplicate, not a second source of truth. Use one or the other, not both.

POST /api/wallet/pending-transaction exposes the same object under data.transaction.transactionStatus (single lookup) and data.transactionList[].transactionStatus (paginated list).

POST /api/v2/transfers/list does not include transactionStatus.

transactionStatus fields

FieldTypeDescription
legacyStatusintegerDuplicates the existing top-level status field.
primaryStatusstringOne of pending, success, failed. Maps directly to legacyStatus
secondaryStatusstringGranular lifecycle sub-status — see the table below.

Full secondaryStatus reference

Not all secondaryStatus values below are fully available yet. If you don't yet see one you expect, check with your Liminal integration contact.

secondaryStatuslegacyStatusprimaryStatusStage
awaiting_approval1pendingInitiation
awaiting_firewall_approval1pendingFirewall
firewall_finalised1pendingFirewall
awaiting_signatures1pendingSigning — eligible for submit
signing_in_progress1pendingSigning — submit accepted
fully_signed1pendingSigning complete
awaiting_broadcast2pendingQueued for broadcast
awaiting_receipt2pendingBlockchain
awaiting_rebroadcast2pendingBlockchain
awaiting_resend2pendingBlockchain
awaiting_confirmation2pendingBlockchain
awaiting_finalisation2pendingBlockchain
completed4successDone
failed_pre_broadcast5failedSigning failed before broadcast
broadcast_rejected5failedRejected at broadcast
failed_on_chain5failedReverted/failed on chain
expired_before_broadcast5failedExpired
rejected_by_approver5failedInternal approval rejected
rejected_in_firewall5failedFirewall/policy rejected
rejected_by_signer5failedSigner rejected
policy_checks_failed5failedPolicy check failed
cancelled_by_initiator5failedCancelled
cancelled_by_user5failedCancelled
dropped5failedDropped
superseded5failedSuperseded
cancel_pending1pendingCancellation in progress

Submitting a transaction: POST /api/v2/transactions/submit

For the transaction you submit:

  • It's signed and submitted for processing as part of this call.
  • Nonce reordering is handled automatically if you submit transactions out of order — you don't need to submit in nonce order.

There's no separate signing step for you to perform. Signing and submission both happen as part of this call: the response tells you whether the transaction was processed or failed.

Reaching processed means signing and submission succeeded. Broadcast, confirmation, and finalization still happen asynchronously afterward — keep polling transaction status to see it through to completion.

Scope: V2 MPC wallets (hot, deposit), on any chain. Not available for multisig V2 wallets, V1 wallets, or wallets that sign via hardware, HSM, or offline workflows.

Request

FieldTypeRequiredDescription
walletIdintegerYesThe wallet the transaction belongs to. Must be a positive integer.
sequenceIdsstring[]YesThe sequence ID(s) to submit. Non-empty, no duplicates.
{
  "walletId": 12345,
  "sequenceIds": ["seq-A"]
}

Response — 200 OK

{
  "processed": ["seq-A"],
  "failed": []
}
FieldTypeDescription
processedstring[]sequenceIds that were assigned a nonce, signed, and submitted successfully.
failed[].sequenceIdstringA sequenceId that did not complete.
failed[].messagestringWhy it didn't complete. Text varies by failure point, so treat this as informational rather than a fixed set of values.

A non-empty failed array does not mean the call errored. This endpoint returns 200 as long as it got far enough to attempt the transaction. HTTP-level errors (see Errors, below) only happen when something is wrong before the transaction is attempted at all — a bad request, or a wallet that's out of scope.

What to do with the response

Nothing to sign. If the sequenceId came back in processed, poll transaction status to track it through to completed. If it came back in failed, see Retrying below.

Retrying after a failure

Once a sequenceId has succeeded, don't include it in a later submit call — a sequenceId that's already past the signing stage is no longer eligible, and including it fails the whole call. Retry only the sequenceId(s) that came back in failed:

{ "walletId": 12345, "sequenceIds": ["seq-A"] }

Retrying is otherwise safe — resubmitting a sequenceId that only failed at signing or submission (not at nonce assignment) won't cause a double-spend.

Submitting more than one transaction at a time

Most integrations submit one sequenceId at a time. If you need to, you can submit up to 5 pending transactions in a single call by listing multiple sequence IDs:

{
  "walletId": 12345,
  "sequenceIds": ["seq-A", "seq-B"]
}

Each sequenceId is processed independently, and the response reports the outcome per sequenceId:

{
  "processed": ["seq-A"],
  "failed": [
    { "sequenceId": "seq-B", "message": "Submit gate returned no unsigned payload" }
  ]
}

A batch may span more than one source address — each address just needs to already be registered to the wallet.

Authorization

Requires valid API credentials. Missing or invalid credentials return 401.


Errors

Errors follow Liminal's standard response envelope:

{
  "success": false,
  "message": "walletId must be a positive integer",
  "code": "VALIDATION_ERROR"
}

Match on code where one is present. Where code is empty, use the HTTP status.

Error Codes

Error messageHTTP statuscodeCondition
Multiple error messages400VALIDATION_ERRORRequest Body validation errors
"A submit batch may carry at most 5 transactions"400sequenceIds exceeds the batch limit
"Submit flow applies only to MPC v2 wallets on account-nonce chains"400Wallet is not a V2 MPC wallet
"All transactions must be awaiting signatures to be submitted"400One or more transactions aren't in an eligible state to start signing
"Submit flow is disabled"404Feature not enabled for your organization
"One or more sequenceIds were not found for this wallet"404TRANSACTION_NOT_FOUNDA requested sequenceId doesn't belong to walletId
"Transaction request not found for sequenceId {{sequenceId}}"404TRANSACTION_NOT_FOUNDThe transaction record exists but isn't ready to resolve
"Nonce assignment could not be completed for sequenceId {{sequenceId}}"409Transient contention. Retry.
"Transaction {{sequenceId}} is not eligible for nonce assignment (sub_status: {{subStatus}})"409Unexpected state. Rare.
"Failed to re-prebuild transaction for sequenceId {{sequenceId}}"500Server-side error refreshing the transaction. Retry.

If you hit one of these, nothing in the request was signed, and there's no processed/failed body — fix the request and resubmit.

An individual sequenceId can also end up in the failed array of a normal 200 response instead of causing one of the errors above — that happens when the request as a whole was accepted but one or more transaction didn't make it through signing or submission. See Retrying, above.



Did this page help you?