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:
- Create the withdrawal. Call
POST /api/v2/wallets/:walletId/send-many-transaction-requestwith a uniquesequenceId. - Wait for pre-signing checks. Liminal runs balance, address, and policy/firewall checks asynchronously.
- Poll the status of the transaction. Use
POST /api/wallet/transaction-statusand wait forlegacyStatus: 1,primaryStatus: "pending",secondaryStatus: "awaiting_signatures"— this means the transaction is ready to be submitted. - Submit it. Call
POST /api/v2/transactions/submitwithwalletIdand thesequenceIdyou want to move forward. - Read the result. The response tells you whether the sequenceId was
processedorfailed, with a reason if it failed. Nothing further to sign. - Confirm the outcome. Either keep polling transaction status until
legacyStatusreaches4(primaryStatus: "success") or5(primaryStatus: "failed"), or rely on a transaction webhook instead — polling isn't required if you've configured one. UselegacyStatus/primaryStatusfor this final check rather thansecondaryStatus— 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
transactionStatusPOST /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
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/listdoes not includetransactionStatus.
transactionStatus fields
transactionStatus fields| Field | Type | Description |
|---|---|---|
legacyStatus | integer | Duplicates the existing top-level status field. |
primaryStatus | string | One of pending, success, failed. Maps directly to legacyStatus |
secondaryStatus | string | Granular lifecycle sub-status — see the table below. |
Full secondaryStatus reference
secondaryStatus referenceNot all
secondaryStatusvalues below are fully available yet. If you don't yet see one you expect, check with your Liminal integration contact.
secondaryStatus | legacyStatus | primaryStatus | Stage |
|---|---|---|---|
awaiting_approval | 1 | pending | Initiation |
awaiting_firewall_approval | 1 | pending | Firewall |
firewall_finalised | 1 | pending | Firewall |
awaiting_signatures | 1 | pending | Signing — eligible for submit |
signing_in_progress | 1 | pending | Signing — submit accepted |
fully_signed | 1 | pending | Signing complete |
awaiting_broadcast | 2 | pending | Queued for broadcast |
awaiting_receipt | 2 | pending | Blockchain |
awaiting_rebroadcast | 2 | pending | Blockchain |
awaiting_resend | 2 | pending | Blockchain |
awaiting_confirmation | 2 | pending | Blockchain |
awaiting_finalisation | 2 | pending | Blockchain |
completed | 4 | success | Done |
failed_pre_broadcast | 5 | failed | Signing failed before broadcast |
broadcast_rejected | 5 | failed | Rejected at broadcast |
failed_on_chain | 5 | failed | Reverted/failed on chain |
expired_before_broadcast | 5 | failed | Expired |
rejected_by_approver | 5 | failed | Internal approval rejected |
rejected_in_firewall | 5 | failed | Firewall/policy rejected |
rejected_by_signer | 5 | failed | Signer rejected |
policy_checks_failed | 5 | failed | Policy check failed |
cancelled_by_initiator | 5 | failed | Cancelled |
cancelled_by_user | 5 | failed | Cancelled |
dropped | 5 | failed | Dropped |
superseded | 5 | failed | Superseded |
cancel_pending | 1 | pending | Cancellation in progress |
Submitting a transaction: POST /api/v2/transactions/submit
POST /api/v2/transactions/submitFor 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
| Field | Type | Required | Description |
|---|---|---|---|
walletId | integer | Yes | The wallet the transaction belongs to. Must be a positive integer. |
sequenceIds | string[] | Yes | The sequence ID(s) to submit. Non-empty, no duplicates. |
{
"walletId": 12345,
"sequenceIds": ["seq-A"]
}Response — 200 OK
200 OK{
"processed": ["seq-A"],
"failed": []
}| Field | Type | Description |
|---|---|---|
processed | string[] | sequenceIds that were assigned a nonce, signed, and submitted successfully. |
failed[].sequenceId | string | A sequenceId that did not complete. |
failed[].message | string | Why 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 message | HTTP status | code | Condition |
|---|---|---|---|
Multiple error messages | 400 | VALIDATION_ERROR | Request Body validation errors |
"A submit batch may carry at most 5 transactions" | 400 | — | sequenceIds exceeds the batch limit |
"Submit flow applies only to MPC v2 wallets on account-nonce chains" | 400 | — | Wallet is not a V2 MPC wallet |
"All transactions must be awaiting signatures to be submitted" | 400 | — | One or more transactions aren't in an eligible state to start signing |
"Submit flow is disabled" | 404 | — | Feature not enabled for your organization |
"One or more sequenceIds were not found for this wallet" | 404 | TRANSACTION_NOT_FOUND | A requested sequenceId doesn't belong to walletId |
"Transaction request not found for sequenceId {{sequenceId}}" | 404 | TRANSACTION_NOT_FOUND | The transaction record exists but isn't ready to resolve |
"Nonce assignment could not be completed for sequenceId {{sequenceId}}" | 409 | — | Transient contention. Retry. |
"Transaction {{sequenceId}} is not eligible for nonce assignment (sub_status: {{subStatus}})" | 409 | — | Unexpected state. Rare. |
"Failed to re-prebuild transaction for sequenceId {{sequenceId}}" | 500 | — | Server-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.
Updated about 16 hours ago
